/** 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. */ //Log Dir http://docs.unity3d.com/Manual/LogFiles.html using UnityEngine; using System.Collections; using System.Collections.Generic; using System; using System.Threading; using System.IO; using System.Text; using System.Runtime.InteropServices; 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; // 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; // Gerenciador de legendas public Subtitle subtitles = null; private bool[] lastLetterAnimations = new bool[256]; 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); 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("loadAndPlay"); loading = false; stopAnimations(); } /* Para animações */ public void stopAnimations() { StopCoroutine("handleStates"); this.randomAnimations.unlockFor("handleStates"); 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); } private System.Object LOCKER_PLAY = new System.Object(); /* Reproduz apenas se não houver algo sendo carregado ou rerpoduzido */ public bool playIfEmpty(string gloss) { lock (LOCKER_PLAY) { if (this.loading || this.playing) return false; StartCoroutine("loadAndPlay", gloss); } return true; } /* Enfieira animações para reprodução */ public void playQueued(string gloss) { lock (LOCKER_PLAY) { Debug.Log("GPM:pQ(" + gloss + ")"); StartCoroutine("loadAndPlay", gloss); } } /* Para todas as animações, limpa a fila e reproduz */ public void playNow(string gloss) { lock (LOCKER_PLAY) { stopAll(); StartCoroutine("loadAndPlay", gloss); } } /* Reproduz animação de intervalo */ public bool playIntervalAnimation(string name) { if ( ! Monitor.TryEnter(LOCKER_PLAY)) return false; lock (LOCKER_PLAY) { 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++) { lastAnimationSubtitle = Subtitle.highlight(word, i); char anim = word[i]; switch (word[i]) { case 'Á': case 'Â': case 'À': case 'Ã': anim = 'A'; break; case 'É': case 'Ê': anim = 'E'; break; case 'Í': anim = 'I'; break; case 'Ó': case 'Ô': case 'Õ': anim = 'O'; break; case 'Ú': anim = 'U'; break; } 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(); } UnityEngine.Debug.Log("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 aninmaçã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); } } private System.Object LOCKER_LOADING = new System.Object(); /* Carrega animações e reproduz */ private IEnumerator loadAndPlay(string gloss) { lock (LOCKER_LOADING) { Debug.Log("GPM:lAP(" + gloss + ")"); this.randomAnimations.lockFor("loadAndPlay"); this.loading = true; // onPlayingStateChange(); string lastAnimationSubtitle = ""; bool spelled = false; if ( ! this.playing) StartCoroutine("handleStates"); 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) { 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("Bundle \"" + aniName + "\" loaded!"); } else UnityEngine.Debug.Log ("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("~~ 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); } // Default playAnimation(Subtitle.TYPE_NONE, DEFAULT_ANIMATION, ""); this.loading = false; // onPlayingStateChange(); this.randomAnimations.unlockFor("loadAndPlay"); } } private System.Object LOCKER_PLAYING = new System.Object(); /* Sincroniza as legendas com as animações. */ IEnumerator handleStates() { lock (LOCKER_PLAYING) { this.randomAnimations.lockFor("handleStates"); 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)) { 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; } } this.subtitles.setText(""); resetStates(); this.randomAnimations.unlockFor("handleStates"); } } public void resetStates() { this.animationPlaying = null; this.playing = false; this.paused = false; onPlayingStateChange(); } }