Unity Package Manager e GIT

Package Manager

Unity Package Manager (upm) é uma ferramenta muito interessante que nos permite modularizar e organizar melhor nossos projetos. Está com o editor Unity3D desde a versão 2017.2, mas na versão 2018.1 que ganha a interface e fica realmente integrado, como é apresentado neste post oficial.

Particularmente, utlizo o Package Manager extensivamente para modularizar e organizar melhor os meus próprios projetos; podendo atualizar e reutilizar projetos chave. Por exemplo, um package “util” que é utilizado e melhorado em vários projetos diferentes. Ou um package “SaveSystem” que pode ser reutilizado em qualquer projeto Unity.

O package manager tem diferentes utilidades, desde gerenciar as features básicas da Unity, importar outros packages da asset store ou diretamente de um projeto GIT a partir de um repositório qualquer – e esse é foco desse post. Vamos ver o passo-a-passo de como utilizar o Unity Package Manager para integrar com um projeto open source Git. E, como bonus, a utilizar o Gitlab CI para automatizar a publicação e atualização do pacote.

Para abrir o Package Manager, Vá em “Window” e em seguida “Package Manager”.

Resumo

  • Crie e versione o projeto “pacote” (localmente ou remotamente)
  • Crie uma pasta dentro de “Assets” que será o pacote em si.
  • Adicione o arquivo package.json e crie um “Assembly Definition” nesta pasta
  • Opcionalmente, se utilizar LFS, adicione “.gitattributes” na pasta do pacote criada

Projeto e versionamento

Crie um novo projeto normalmente. Esse será nosso projeto “pacote“. Vamos usar um projeto chamado “UnityUtils” como exemplo.

Crie um novo projeto

Git init

Utilizaremos o GIT para versionar o projeto e o gitlab como repositório. Instale a versão mais recente do git no seu computador e faça uma conta no gitlab.com. O git tem vários clientes (softwares que utilizam o git de forma visual) disponíveis na página oficial. Podemos utilizar qualquer um dos clientes que sentir mais confortável ou diretamente por linha de comando. Neste tutorial utilizarei os comandos git puro, através do console/bash que acompanha o software. Clique com o botão direito e escolha “Git Bash”, isso abrirá um console diretamente na pasta selecionada.

Botão direito e escolha a opção “Git Bash Here”

Para iniciar o repositório git, utilizamos o comando “git init”. Esse comando monta a estrutura Git e inicializa um branch chamado “master“.

> git init
comando “git init” no bash

Git Ignore

A partir de agora podemos adicionar arquivos para o versionamento. Contudo não queremos adicionar todos os arquivos do diretório. Alguns arquivos são temporários, locais ou podem/devem ser recriados pela própria Unity ou IDE. Para ignorar e não adicionar esses arquivos utilizamos um arquivo chamado .gitignore (um arquivo de texto, com ponto na frente e sem extensão). Existem vários modelos desse tipo de arquivo para versionar corretamente os projetos da unity. Eu gosto do seguinte modelo, que pode ser encontrado/baixado aqui. Adicione o arquivo à raiz do projeto.

[Se o projeto for utilizar arquivos grandes, pode ser uma boa ideia configurar o LFS (large file system), aqui tem um arquivo de configuração .gitattributes. Quando trabalhar com LFS é necessário adicionar o arquivo .gitattributes também à raiz do pacote (não somente na raiz do projeto) – para que o Git/Unity, quando estiver resolvendo o pacote, saiba quais tipos de arquivo precisam ser tratados como LFS. Os detalhes do LFS não serão abordados nesse tutorial]

#.gitignore
#
######################
## Project Specific ##
######################
# put here some project specific
# files that can or should be ignored

###############################
## Unity's folders and files ##
###############################

Assets.meta
[Ll]ogs/
[Aa]ssets/AssetStoreTools*
[Bb]uild/
[Bb]uilds/
[Ll]ibrary/
[Ll]ocal[Cc]ache/
[Tt]emp/
[Oo]bj/
[Uu]nityGenerated/
sysinfo.txt


############################
## Autogenerated projects ##
############################

Assets/Plugins/Editor/JetBrains*
[Ee]xportedObj/
*.csproj
*.csproj.meta
*.sln
*.suo
*.userprefs
*.pidb
*.pidb.meta
*.unityproj
.vs
.idea


#########################
## Miscellaneous files ##
#########################

*.swp
Thumbs.db
Thumbs.db.meta
*.blend1
*.blend1.meta

Git Commit

Agora podemos adicionar nossos arquivos e fazer nosso primeiro commit. Os comandos à seguir adicionam os arquivos para o commit e fazem um commit com a mensage “first commit”:

> git add .
> git commit -m "first commit"
git status & git add .
git commit -m “first commit” & git status

Agora, crie um projeto no gitlab. Vou chamar, por exemplo, de “upackage-tutorial“. Na página inicial o próprio gitlab dará dicas de como proceder com o primeiro commit – Se for utilizar SSH, é necessário adicionar a chave pública no gitlab. Também é possível utilizar HTTPS.

Git push

Nós devemos, basicamente, configurar nosso nome e adicionar um “remote” chamado origin com nossa url:

> git config --global user.name "Matheus"
> git config --global user.email "seuemail@provedor.com"

> git remote add origin git@gitlab.com:username/projectname.git
ou
> git remote add origin https://gitlab.com/username/projectname.git

> git push -u origin master
git remote add origin URL & git push -u origin master

Nosso projeto ainda não é um pacote, mas já está versionado com GIT e pronto para o próximo passo. Crie um outro projeto em um outro local – esse utilizará nosso pacote – vamos chamar de “Teste“. Este novo projeto não precisa ser versionado (mas é sempre uma boa ideia versionar todos os nossos projetos).

Package

Agora que temos o nosso projeto “UnityUtils” criado e versionado, vamos criar o conteúdo do “Package”. Em geral o seu projeto terá vários arquivos, cenas e diversos testes que não são interessantes para o pacote em si. Eu, particularmente, gosto de adicionar uma pasta com o nome da empresa ou do projeto onde os arquivos fundamentais estarão e que será considerada o “pacote” de fato. Por exemplo, nosso pacote será a pasta “UnityUtils”:

Hierarquia do Projeto

Desta forma podemos, posteriormente, adicionar outras pastas que não farão parte do pacote; como por exemplo “Tests” ou “Examples”

Assembly Definition e package.json

Transformar essa pasta em “package” não é difícil. Ela precisa conter um arquivo package.json e um assembly definition. O Json é um manifesto definindo pelo menos o nome do pacote, uma descrição, a versão e a partir de qual unity ele é suportado. Obs.: o nome do pacote segue uma convenção (começa com “com”, o nome da empresa e o nome do pacote) e tem outros detalhes que podem ser averiguados no manual.

{
	"name":"com.matheus.unityutils",
	"displayName":"Matheus Unity Utils",
	"version": "1.0.0",
	"unity": "2018.4",
	"description": "Unity utility toolkit"
}

Agora crie o assembly definition e nomei-o.

Na pasta UnityUtils temos dois novos arquivos “package.json” e “UnityUtils.asmdef”

Está pronto o nosso pacote! Ele já pode ser utilizado em outros projetos!

Apesar do pacote estar pronto, ele está disponível somente localmente. Para utilizar ele a partir de qualquer lugar, devemos fazer um commit e um “push especial”.

> git add .
> git commit -m "package files"
> git push 

Com esses comandos mandamos ao servidor nossas modificações. Nada de especial. A seguinte linha publicará o pacote que utilizaremos no Unity Package Manager:

> git subtree push --prefix Assets/UnityUtils origin upm --squash

Esse comando fará um novo “branch” no repositório, chamado “upm” a partir da pasta “Assets/UnityUtils”, nossa pasta pacote.

git commit -m “package files” & git push & git subtree push --prefix Assets/UnityUtils origin upm --squash

Como será um comando bastante utilizado, podemos adicioná-lo à um arquivo “deploy.sh”. Um arquivo de texto contendo aquela linha de comando unicamente. Agora sempre que “deploy.sh” for executado, o pacote será “publicado”

Utilizando o Pacote

No projeto “Test” vamos utilizar este pacote. Para isso abra o arquivo “manifest.json”, que está dentro da pasta “Packages” (pasta irmã de “Assets”, não é filha). Adicione, no manifesto, o nosso pacote como dependência. A referencia pode ser local, no sistema de arquivo do computador, ou remota pelo nosso repositório na nuvem:

  • Localmente:

"com.unityutils": "file:/workspace/utils/Assets/UnityUtils"

  • Remotamente

"com.unityutils": "https://gitlab.com/z3r0_th/unity-utils.git#upm"

Salve a edição. O arquivo manifest.json deveria ficar parecido com isso:

edição do arquivo manifest.json

Quando abrir a Unity3D com o manifesto modificado, a Unity irá clonar o projeto para a pasta Library\PackageCache E o projeto ficará visível na pasta “Packages” dentro da unity

Editor Assembler

Uma observação válida de se fazer é que o Assembly adicionado no pacote tem alguns perks a se levar em consideração. Um deles é que é necessário adicionar um Assembly definition para a pasta “Editor”, caso tenha uma. A definição desse Assembly é um pouco diferente, já que precisamos avisar o compilador, que este será um código somente para o Editor:

definição de um Assembly definition para uma pasta “Editor”, por exemplo

Note que, se temos referencias a scripts que estão em outras pastas, com outras definições de assembly, precisamos referenciar na lista “Assembly Definition References”; caso contrário teremos erros de compilação infomando que não foi possível encontrar classes e referencias.

Bonus: Gitlab CI

CI significa “Continuos Integration” e quer dizer que podemos automatizar a publicação através do nosso sistema de versionamento utilizando alguns serviços, regras e arquivos. O gitlab oferece algumas featuers e vamos utilizá-las para automatizar o processo de “deploy” do nosso pacote. Queremos que, quando fizermos um “push” para o nosso branch “master”, nosso projeto faça um push/deploy para nosso branch “upm” que é utilizado na unity. Crie um arquivo chamado “.gitlab-ci.yml” e adicione o seguinte texto, modificando para sua necessidade (troque o nome da pasta no comando git subtree split –prefix Assets/UnityUtils -b upm –squash, basicamente):

stages:
- deploy
Unity-Package:
stage: deploy
only:
- master
before_script:
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
- 'git-lfs version || ( apt-get update -y && apt-get install git-lfs )'
- eval $(ssh-agent -s)
- echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | ssh-add - > /dev/null
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- git config --global user.email "gitlabci@gitlab.com"
- git config --global user.name "Git lab ci"
- gitlab_hostname=$(echo "${CI_REPOSITORY_URL}" | sed -e 's|https\?://gitlab-ci-token:.@||g' | sed -e 's|/.||g')
- ssh-keyscan "${gitlab_hostname}" >> ~/.ssh/known_hosts
- chmod 644 ~/.ssh/known_hosts
script:
- url_host=$(echo "${CI_REPOSITORY_URL}" | sed -e 's|https\?://gitlab-ci-token:.*@|ssh://git@|g')
- git branch
- git subtree split --prefix Assets/UnityUtils -b upm --squash
- git checkout upm --force
- git remote set-url --push origin "${url_host}"
- git push origin -u --force upm

Também devemos adicionar uma variável no CI do git lab. Acesse as configurações do projeto:

Settings -> CI/CD

Devemos criar um par de chave ssh que será nosso “publicador”. Crie uma nova chave ssh. Siga o passo-a-passo que os repositórios fornecem, como este da atlassian ou este do gitlab. Caso já tenha feito uma chave ssh, é importante fazer backup ou, melhor ainda, gerar a chave em outro path.

Adicione uma variavel secreta para o projeto, chamada “SSH_PRIVATE_KEY”. O “value” deve ser a chave privada que acabamos de criar.

A chave pública deverá ser adicionada como um “deploy key”, que pode ser encontrado em “repositórios”

Fade Unity3D Tutorial

Introdução

A habilidade de fazer fade de elementos da interface é interessante. Pode-se, por exemplo, fazer transições sutis e fazer apresentações mais bonitas dos componentes do game. É uma animação básica e não pode faltar no nosso arsenal de ferramentas.

Este tutorial irá mostrar como criar um Fader para Images (ou Graphic) e CanvasGroup da Unity3D. Cobrindo o uso de corotinas para o controle da transição e o callback quando terminar o processo. Será possível tanto chamar uma função diretamente de um objeto editado no Editor, quanto chamar uma função para um objeto específio.

// Função estática para fazer um fade em cima de uma Image
Fader.FadeOut(image, time, delegate(var img) {Debug.Log("Finished");});

//Chamada da função fade diretamente em um objeto
Fader imageFade;
imageFade.Fade();

Como bônus veremos um Fader para a tela toda.

De qualquer maneira, o código pode ser encontrado no repositório: unity-utilities, com mais opções (como Easing e um custom editor)

Fading images, canvas group and screen, tela imagens e grupo

Fading images, canvas group e screen. Retirado do repositório unity-utilities

Métodos e Propriedades

Vamos definir as propriedades e métodos do nosso Fader. Ele deve ter pelo menos um componente gráfico: CanvasGroup ou Graphic. E irá transacionar o alpha desse  componente, do alpha atual ao desejado, em um determinado tempo. Essa transição pode ser acionada diretamente pelo componente, ou por script.

Código

Com isso em mente podemos declarar os seguintes métodos e variáveis:


//Alpha destino. Pode ser 0 para um fade out, ou 1 para um fade in.
//Alpha deve ser entre 0 e 1. Por isso usamos o atributo Range
[SerializeField]
[Range(0,1)]
private float alphaTarget = 0;

//Tempo para transição do alpha
[SerializeField]
private float time = 1;

//Evento de callback editável no Editor da Unity3D
[SerializeField]
private FaderCompleteEvent fadeCompleted = new FaderCompleteEvent();

//Somente usaremos ou imagem ou group
//Como uma imagem é um Graphic, podemos usar Graphic no lugar de Imagem
//A vantagem de usar Graphic é que esse script poderá ser usado para
//outros componentes que estendem Graphic (por exemplo RawImage)
private Graphic image;
private CanvasGroup group;

//Metodos

//Faz o fade do alpha atual para o alpha destino no tempo determinado
public void Fade();

//Podemos declarar um outro método que faz fade para um alha diferente
//e outro que faz fade em um alpha diferente com um tempo diferente.
//Como a assinatura (parâmetros passados - quantidade e/ou tipo)
//são diferentes o método pode ter o mesmo nome
public void Fade(float alpha);
public void Fade(float alpha, float time);

//Fade out e Fade in são bem comuns. Por isso é interessante ter auxiliares para esses métodos.
public void FadeIn(float time);
public void FadeOut(float time);
public void FadeIn();
public void FadeOut();

//Podemos ter métodos estáticos para fazer a transição fade a partir de qualquer outro script.
public static void Fade(Graphic graphic, float alpha, float time, Action callback);
public static void Fade(CanvasGroup canvasgroup, float alpha, float time, Action callback);

Temos nossos métodos definidos.

Explicando

O campo alphaTarget, por representar o alpha desejado, só pode ser de 0 à 1. Por isso usamos o atributo Range, para disponibilizar um slide que restringe a escolha do usuário, no editor da Unity, de 0 à 1.

Quando usamos o atributo SerializeField em um campo privado, a Unity irá serializar o campo e permitir a edição da variável (se for possível para aquele tipo de variável) no Editor.

FaderCompleteEvent é um evento da Unity. Temos um post explicando melhor a declaração e o uso desse tipo de classe. Omitimos parte da declaração desse campo (não se preocupe, mais abaixo mostramos o código completo). O efeito desse campo é permitir que o usuário defina os callbacks no Editor bem parecido com o botão da Unity.

Exemplo de UnityEvent e Range Attribute

Exemplo de UnityEvent e Range Attribute

Os campos image e group, Graphic e CanvasGroup respectivamente, são os componentes que iremos modificar. Não estão públicos ou serializados. Iremos pegar os componentes dinamicamente para conveniência.

Dos métodos temos vários métodos com o mesmo nome, mas com Assinatura diferente. O método Fade, por exemplo, faz o fade do componente conforme os parâmetros definidos. Contudo podemos redefinir alguns parâmetros passando outros valores para o método Fade.

Adicionamos os métodos auxiliares FadeIn e FadeOut, de uso bastante comum. O que eles fazem é simplesmente redefinir o alpha atual e o alphaTarget para ter o efeito desejado.

Os métodos estáticos devem aplicar o script à um componente específico. Assim não é necessário ter o script adicionado ao componente de antemão.

Implementação

Iniciamos declarando a classe, nosso métodos, propriedades e classe para o evento. Iremos também setar os campos image e group no método Awake (chamado logo após o script estar ativo).

Código

using System.Collections;
using UnityEngine.UI;
using UnityEngine;
using System;
//Nossa Classe de Exemplo
public class FaderExample : MonoBehaviour {
//Classe interna auxiliar para tratar os callbacks
[System.Serializable]
public class FaderCompleteEvent : UnityEngine.Events.UnityEvent { }
//Acesso publico ao callback
public FaderCompleteEvent FadeCompleted
{
     get
     {
           return fadeCompleted;
     }
}
//Possivel editar os eventos no  Editor (como um botão)
[SerializeField]
private FaderCompleteEvent fadeCompleted = new FaderCompleteEvent();

[SerializeField]
[Range(0,1)]
private float alphaTarget;

[SerializeField]
private float time;

private CanvasGroup group;
private Graphic graphic;
private float timeCount;
//Primeira coisa que fazemos é pegar os componentes
//E verificar se são validos
private void Awake()
{
     group = GetComponent<CanvasGroup>();
     graphic = GetComponent<Graphic>();
     if (group == null && graphic == null)
     {
          Destroy(this);
          Debug.LogError("Não há um componente CanvasGroup ou Graphic. Por favor cheque esse bug.");
     }
}
//Os métodos fade são auxiliares para chamar o  método Fade
//principal com algumas facilidades
public void Fade()
{
     Fade(alphaTarget, time);
}

public void Fade(float alpha)
{
     Fade(alpha, time);
}
//Troca o nosso alpha atual para 0 e o target para 1
//Note que não modificamos a configuração da classe em si
public void FadeIn(float time)
{
     if (group != null) group.alpha = 0;
     if (graphic != null) graphic.color = Color.clear;
float _alphaTarget = 1;
     Fade(_alphaTarget, time);
}
//idem, mas com a propriedades para fadeout
public void FadeOut(float time)
{
     if (group != null) group.alpha = 1;
     if (graphic != null) graphic.color = Color.white;
float _alphaTarget = 0;
     Fade(_alphaTarget, time);
}
//Esse método faz um "wrap" para o método principal
//Chamando uma corotina.
public void Fade(float alpha, float time)
{
     StartCoroutine(DoFade(alpha, time));
}
//Método estático auxiliar
//Irá adicionar ao componente um FaderExample e configurar os campos
//Por fim chamando o método Fade e destruindo o script depois que
//terminar o processo.
public static void Fade(Graphic graphic, float alphaTarget, float time, Action callback)
{
      if (graphic == null) { Debug.LogError("Não há gráfico. Abortar"); return; }
      FaderExample example = graphic.gameObject.AddComponent<FaderExample>();

      example.graphic = graphic;
      example.time = time;
      example.alphaTarget = alphaTarget;

     if (callback != null)
     {
           Action _callback = callback;
           example.fadeCompleted.AddListener(delegate { _callback(); });
     }

     example.Fade();
     Destroy(example, time + 0.1f);
}
//idem
public static void Fade(CanvasGroup canvasGroup, float alphaTarget, float time, Action callback)
{
     if (canvasGroup == null) { Debug.LogError("Não há canvasGroup. Abortar"); return; }
     FaderExample example = canvasGroup.gameObject.AddComponent<FaderExample>();

     example.group = canvasGroup;
     example.time = time;
     example.alphaTarget = alphaTarget;

     if (callback != null)
     {
          Action _callback = callback;
          example.fadeCompleted.AddListener(delegate { _callback(); });
     }

     example.Fade();
     Destroy(example, time + 0.1f);
}

//Método principal a ser implementado
private IEnumerator DoFade(float alphaTarget, float time)
{
// Implements
yield break;
}
}

Ok. Esta é a nossa estrutura básica. O que falta é apenas implementar o método DoFade. Que faremos em seguida depois  da explicação do  código acima.

Explicando

Como explicado no post anterior, sobre UnityEvent, a declaração da classe interna e do objeto FaderCompleteEvent serve para permitir a edição dos callbacks pelo Editor da Unity3D.

Além das demais variáveis, já comentadas e explicadas temos o método Awake, que irá tentar “pegar” o componente Graphic e CanvasGroup e verificar se existe ou não pelo menos um componente válido diferente de null.

Observe também que todos os métodos Fade são, de uma maneira ou de outra, uma forma encapsular o método que irá, de fato, executar o algorítmo em uma corotina: DoFade.

Os métodos estáticos para fazer um Fade são usados apenas para encapsular a inicialização e configuração de um sript FaderExample. Depois do tempo de fade destruimos o script associado.

Implementação Corotina DoFade

É importante lembrar que a corotina não é executada em paralelo no sentido multithread. Uma corotina, na unity, é executada todo frame, logo após o método Update. Existe mais detalhes sobre corotina, uma feature bastante interessante de entender e usar dentro da Unity, mas não cabe neste post. O importante é saber que a corotina DoFade será chamada todo frame e irá tomar controle da execução até liberar chamando o comando yield.

Nós iremos usar a função Mathf.Lerp. Essa função faz uma interpolação linear de um valor à outro baseado em uma “porcentagem” (valor de 0 à 1). Por exemplo, se formos interpolar um valor de 10 à 100, se o valor da interpolação for 0, o valor retornado será 0, se for 1 o valor será 100 e se for 0.5, 45 e assim por diante. Usaremos essa função para definir o valor de alpha em determinado tempo.

Acumulamos o tempo em uma variável timeCount. Para isso, basta adicionar Time.deltaTime todo frame. Quando timeCount for 0, queremos pegar o valor inicial de alpha, e quando for o próprio valor de time definido no script, aplha deve ser o valor final. Para isso precisamos normalizar o tempo para que fique um valor entre 0 e 1. Podemos simplesmente dividir timeCount por time.

Vamos ao Código

Código

private IEnumerator DoFade(float alphaTarget, float time)
{
//Verifica se há pelo menos um componente válido
     if ((graphic == null && group == null))
     {
          yield break;
     }
//Definimos o valor de alpha inicial
//Se existe um CanvasGroup (group) pegamos o seu alpha
//Se não, pegamos o alpha do Graphic (graphic)
     float initialAlpha = group != null ? group.alpha : graphic.color.a;

//O alpha final é o valor de alphaTarget passado
     float finalAlpha = alphaTarget;

//Vamos controlar o tempo com timeCount
     float timeCount = 0;
//Enquanto timeCount for menor que time, vamos executar a transição do alpha
     while (timeCount < time)
     {
//timeCount é timeCount + Time.deltaTime até no máximo o próprio "time"
         timeCount = Mathf.Min(timeCount + Time.deltaTime, time);

//Interpolação para o alpha. Dividimos timeCount por time para obter um valor entre 0 e 1
         float alpha = Mathf.Lerp(initialAlpha, finalAlpha, timeCount / time);

//Aplicamos alpha
         if (group != null) group.alpha = alpha;
         if (graphic != null) graphic.color = new Color(graphic.color.r, graphic.color.g, graphic.color.b, alpha);
//Retornamos o controle. Se não houver essa linha, o while
//irá executar até terminar, sem retomar o controle para a Unity
//e não veremos o fade acontecer. Justamente porque a Unity não teve
//a oportunidade de executar um render e atualizar a tela.
         yield return null;
     }
//Acabamos o fade. Invocamos todos os callbacks registrados
     if (fadeCompleted != null)
         fadeCompleted.Invoke();
}

Com essa função terminamos o nosso exemplo de um Fader na Unity3D.

O quê melhorar?

Esse fader é demonstração e tem muito o que melhorar. Por exemplo:

  • Fade com cores. Graphic  tem a propriedade Color color. Poderíamos adicionar a opção de fazer um fade com cor.
  • Editor customizado que diferencie Graphic de CanvasGroup e dê a opção de selecionar a cor em  um e somente o alpha em outro.
  • Fade para texturas e sprites também. Além de elementos da UI, talvez seja interessante ter fade para esses outros elementos
  • Função Ease para criar animações mais interessantes para o Fade. Como Ease in ou Ease out.

 

Obrigado 🙂

 

Bônus ScreenFade

Em breve.

TimeTrigger com Unity Event

UnityEvent

UnityEvent é uma classe da Unity3D que auxilia na edição de callbacks de funções. Ela é bastante útil pois apresenta-se de forma bastante amigável no Editor. É essa classe que o botão da unity3D utiliza para fazer as chamadas, por exemplo.

No repositório open source da UI da Unity3d é possível ver como pode ser utilizado (Classe Button).

unity3d event using button, evento unity3d usando botão

Exemplo de evento usando botão (Button)

TimeTrigger

TimeTrigger é uma classe auxiliar que iremos implementar para chamar funções depois de determinado tempo. Por ser super simples, iremos utilizá-la como exemplo base.

A classe faz parte de um repositório de utensílios para Unity3D e pode ser encontrada aqui.

Implementação

UnityEvent

Como declarar UnityEvent

Para declarar e utilizar o UnityEvent seguimos o exemplo utilizado no próprio botão. Simplesmente declaramos uma classe interna que estende UnityEvent e declaramos a variável local. Assim:


[System.Serializable]
public class TimeTriggerEvent : UnityEvent { }

public TimeTriggerEvent TriggerEvent
{
     get
     {
         return triggerEvent;
     }
}

[SerializeField]
private TimeTriggerEvent triggerEvent = new TimeTriggerEvent();

Observe que com a classe UnityEvent há a possibilidade de ser estendido utilizando template. Permitindo assim a chamada de métodos com parâmetros:


public class TimeTriggerEvent : UnityEvent<float> { }
public class TimeTriggerEvent : UnityEvent<float, string> { }
public class TimeTriggerEvent : UnityEvent<GameObject> { }
//etc.

Utilizamos o Atributo “SerializeField” para serializar a variável e, também, permitir que seja visível e editável no Editor da Unity3D.

Como utilizar UnityEvent

A variável timeTrigger pode conter várias funções de callback de objetos diferentes. Para adicionar um novo callback, pode-se fazer através do editor pelo próprio objeto (como no exemplo do botão no início desse post) ou por script com a função AddListener:

triggerEvent.AddListener(UnityAction);

Para invocar todos os eventos cadastrados basta chamar Invoke e pronto. Assim:

triggerEvent.Invoke();

TimeTrigger

Para este tutorial, vamos ver o básico do TimeTrigger. Basicamente, contamos o tempo e chamamos o evento. Simples assim.

Tic-Tac

Para contar o tempo usamos uma variável auxiliar que incrementamos com Time.deltaTime  – o tempo que levou para completar o ultimo frame. Quando essa variável atingir um determinado limite, invocamos o evento e desabilitamos o script.

private float timeCounter = 0;

[SerializeField]
private float timeTrigger;

void Update()
{
     timeCounter += UnityEngine.Time.deltaTime;
     if (timeCounter >= timeTrigger)
     {
          timeCounter = timeTrigger;
          enabled = false;
          triggerEvent.Invoke();
     }
}

Pronto assim, temos um time trigger usável no próprio Editor:

Time Trigger Event using with button click, Time Trigger Event usando com o click de um botão

Time Trigger Event usando com o clique de um botão

No exemplo da imagem acima, podemos ver que depois de um clique, esperamos um delay e invocamos os eventos, chamando determinadas funções.

Super simples de configurar invocação de funções baseadas em tempo. Poderíamos, por exemplo ter um TimeTrigger para determinar o tempo total de uma Fase, chamando a função de encerramento quando o tempo acabar.

Auxiliar

Podemos, claro adicionar funções auxiliares para facilitar e deixar mais claro algumas possibilidades, por exemplo:


public void ResetTrigger()
{
     timeCounter = 0;
     enabled = true;
}

public float Time
{
     get
     {
          return timeCounter;
     }
}

public float ReverseTime
{
     get
     {
          return timeTrigger - timeCounter;
     }
}

public void Pause()
{
     this.enabled = false;
}

public void Resume()
{
     this.enabled = true;
}

Static Helper

Bom, o que vimos até agora é muito legal para editar através Editor da unity. Seria muito interessante se tivéssemos métodos auxiliares para ajudar a utilizar estas funcionalidades por script também. Métodos estáticos que nos auxiliem a gerenciar a criação e manutenção de objetos TimeTrigger pode ser interessante:

public static TimeTrigger SetTrigger(float time, System.Action callback)
{
     GameObject timergo = new GameObject("TimeTrigger");
     TimeTrigger trigger = timergo.AddComponent<TimeTrigger>();
     trigger.SetTrigger(time, callback, true);
     return trigger;
}

private void SetTrigger(float time, System.Action callback)
{
     System.Action _callback = callback;//There is a bug in some versions where you MUST declare a local variable to use inside a delegate
     this.timeTrigger = time;

     this.triggerEvent.AddListener(delegate ()
     {
        if (_callback != null)
        {
             _callback();
        }
     });
}

Exemplos

Alguns exemplos de utilização por código:


//After 5 seconds, the callback is called.
//After 5 seconds, the console shows "Time Finished"
TimeTrigger timer = TimeTrigger.SetTrigger(5, delegate ()
{
     Debug.Log("Time Finished");
});

timer.Pause();
timer.Resume();
string seconds = timer.Time;
string countDown = timer.ReverseTime;

Exemplo no editor:

Time Trigger Event using with button click, Time Trigger Event usando com o click de um botão

Time Trigger Event usando com o clique de um botão

O que fazer para melhorar

Esse exemplo é muito cru. Tem muito o que melhorar. No repositório talvez é possível encontrar uma versão um pouco mais completa.

De qualquer maneira algumas modificações que talvez sejam interessante:

  • Contagem de tempo independente de Time.deltaTime.
    • Isso porque a contagem com Time.deltaTime é dependente da escala do tempo (algumas vezes modificada para fazer o jogo pausar, efeitos de slow motion, etc)
  • Pooling ou até, talvez, um único contador
    • Para questões de otimização
  • Um gerenciador capaz de pausar e resumir todos os Timers de uma vez
    • Isso poderia facilitar a questão de pausar, resetar, etc
  • Classes complementares para exibir o tempo em textos
    • Converter para minutos/horas/milisegundos, exibir em um Text da unity de forma formatável etc.

Essa foi uma breve introdução ao UnityEvent utilizando a classe TimeTrigger como meio.
Deixe seu feedback. Obrigado pela leitura.

Número randômico simples

Essa é uma ideia muito simples para geração de números pseudo-aleatórios. A implementação, a partir de um seed inicial gera, linear e periodicamente números “aleatórios”. O algoritmo apresentado, é também conhecido como LCG.

Algoritmo


float previous = SEED;
float period = 2e31;
float increment = 1234;
float multiplier = 1103515245;

public float NextRandom()
{
     float next = previous = (multiplier * previous + increment) mod period;
     return next;
}

onde:

previous é o resultado anterior. O primeiro valor de previous é o próprio SEED. next se tornará o próximo previous e assim por diante. O valor  inicial deve ser maior ou igual a zero e menor que o period (algumas vezes chamado de ‘m’ ou módulo). Esse seria o valor atribuído em srand numa linguagem como o C++. Algumas vezes também conhecido como ‘X0’.

increment é uma constante para incremento. Deve estar entre 0 e period. Essa constante pode ser setada para ‘12345’. Algumas vezes também conhecido como ‘c’.

multiplier é um multiplicador. Uma constante entre 0 e period. Pode-se atribuir o valor de ‘1103515245’. Algumas vezes também conhecido como ‘a’.

period é o módulo, onde o valor aleatório não irá ultrapassar. Pode ser tão grande quanto o maior inteiro possível. Para suportar máquinas 32 bits, pode-se atribuir 2e31. Algumas vezes também  conhecido como ‘m’.

next é o valor aleatório gerado. O próximo valor de ‘previous’.

Na wikipedia tem valores atribuídos para esse algoritimo em diversas linguagens e plataformas.

Para gerar um valor entre 0 e 1. Deveríamos dividir o resultado obtido por maxPeriod, o maior valor possível.

O livro “Artificial Intelligence for Humans, Volume 1: Fundamental Algorithms” tem um capítulo inteiro sobre números aleatórios muito interessante.

Path Find A*

Algum tempo atrás precisei utilizar um algoritmo de path find na Unity3D. Implementei o conhecido A*. Compartilho aqui a implementação. Tentei deixar o mais genérico e fácil de usar possível. Qualquer erro, sugestão, ou dúvida com relação ao algoritmo, deixe um comentário.

Algorítimo

using System.Collections.Generic;
using System.Collections;

public class AStar<T> {

	#region Delegates

	public delegate float DistanceInformationAboutTwoTiles(T from, T to);
	public delegate T[] GetNeighboor(T t);
	public delegate bool IsWalkable(T t);

	public DistanceInformationAboutTwoTiles HeuristicDistanceDelegate;
	public DistanceInformationAboutTwoTiles DifficultToWalkDelegate;
	public GetNeighboor GetNeighboorDelegate;
	public IsWalkable IsWalkableDelegate;

	#endregion

	#region Variables

	// Used in search method, to avoid deadlock
        private List<T> closed;
	private List<Node> open;

	// Used as auxiliary, the lastNode will contain the information about the whole path
	private Node lastNode;

	// The target, it should go from start to this path
	private T pathTarget;

	#endregion

	#region Public Methods

	/// <summary>
	/// Initializes a new instance of the AStar class.
	/// </summary>

    public AStar()
    {
		this.closed = new List<T>();
		this.open = new List<Node>();
    }

	/// <summary>
	/// Finds a path from start to end.
	/// If it finds some path connecting start to end, returns true; returns false otherwise
	/// The complete path is set on path parameter
	/// If start is already equal to end it returns a path with start in it.
	/// If no path is found, path will be anything not valid.
	/// </summary>

	public bool FindPath(T start, T end, out T[] path)
    {
		if (start.Equals(end))
		{
			path = new T[]{start};
			return true;
		}

        this.pathTarget = end;

		this.open.Clear();
		this.closed.Clear();
		closed.Add(start);

		bool result = Search(new Node(this, start, end));
		path = RecoverPath(lastNode);

		this.closed.Clear();
		this.open.Clear();

		return result;
    }

	#endregion

	#region Private methods

	/// <summary>
	/// From node, recover the possible path;
	/// In an array that represents the path to peform using generic T.
	/// </summary>

	private T[] RecoverPath(Node node)
    {
        List<T> path = new List<T>();
        while (node != null && node.Parent != null)
        {
            path.Add(node.Tile);
            node = node.Parent;
        }

		if (node != null)
			path.Add(node.Tile);

		path.Reverse();
		return path.ToArray();
    }

	/// <summary>
	/// Main method to find the path.
	/// </summary>

	/// Talvez tenha que passar uma lista de nodos abertos (os vizinhos à ser explorado
	/// e todo loop procura pelo vizinho, dentre todos os vizinhos, com o menor F.
	private bool Search(Node currentNode)
    {
		open.AddRange(NodeNeighboors(currentNode, pathTarget, closed));
		open.Sort((node1, node2) => node2.TotalProbableDistance.CompareTo(node1.TotalProbableDistance));

		for (int i = open.Count - 1 ; i >= 0 && open.Count != 0 ; --i)
        {
			Node node = open[i];
			open.RemoveAt(i);

			if (closed.Contains(node.Tile)) continue;
			closed.Add(node.Tile);

			if (node.Tile.Equals(pathTarget)) {
				lastNode = node;
				return true;
			}
			if (Search(node)) {
				return true;
			}
        }

        return false;
    }

	#endregion

	#region Delegates Method Wrapper

	/// <summary>
	/// Find neighboors of the Tile
	/// </summary>

	protected T[] Neighboors(T tile)
	{
		return GetNeighboorDelegate(tile);
	}

	/// <summary>
	/// Determines whether this node has a tile that is walkable
	/// </summary>

	protected bool IsNodeWalkable(Node search)
	{
		return IsWalkableDelegate(search.Tile);
	}

	/// <summary>
	/// Calculates the real distance from start node to tile node.
	///
	/// The distance from start to "toTile" is the distance from start to "fromNode" plus the distance from "fromNode" to "toTile".
	/// This method uses 'G' of "fromNode" and calculate distance from "fromNode" to "toTile" using delegate.
	/// </summary>

	protected float CalculateG(Node fromNode, T toTile)
    {
		return fromNode.TotalDistanceFromStart + DifficultToWalkDelegate(fromNode.Tile, toTile);
    }

	/// <summary>
	/// Calculates the heuristic distance from tile "from" to target tile "to"
	/// </summary>

	protected float CalculateH(T from, T to)
    {
		return HeuristicDistanceDelegate(from, to);
    }

	/// <summary>
	/// Gets all neighboors that was not visited yet and are walkable.
	/// Returns neighboors sorted by 'TotalProbableDistance' property.
	/// </summary>

	protected Node[] NodeNeighboors(Node node, T target, List<T> closed)
	{
		List<Node> n = new List<Node>();

		T[] neighboors = Neighboors(node.Tile);
		foreach(T tile in neighboors)
		{
			if (closed.Contains(tile)) continue;
			Node info = new Node(this, tile, target, node);
			if (IsNodeWalkable(info)) {
				n.Add(info);
			}
		}

		return n.ToArray();
	}

	#endregion

	#region Node Class

	/// <summary>
	/// Represents one node in the path.
	/// Holds information about how hard is to go through this path
	/// And information about how to compose the whole path
	/// </summary>

	protected class Node
	{
		private float totalDistanceFromStart = 0;
		private float heuristicDistance = 0;

		private T tile = default(T);
		private Node parent = null;

		public Node(AStar<T> star, T index, T targetIndex, Node parent=null)
		{
			this.tile = index;
			this.parent = parent;

			if (parent != null)
			{
				this.totalDistanceFromStart = star.CalculateG(parent, this.tile);
			}

			this.heuristicDistance = star.CalculateH(index, targetIndex);
		}

		public Node Parent
		{
			get
			{
				return this.parent;
			}
		}

		public T Tile
		{
			get
			{
				return this.tile;
			}
		}

		public float TotalDistanceFromStart
		{
			get
			{
				return totalDistanceFromStart;
			}
		}

		public float TotalProbableDistance
		{
			get
			{
				return totalDistanceFromStart + heuristicDistance;
			}
		}

		public override bool Equals( object other )
		{
			Node node = (Node)other;
			if (node == null) return false;
			return this.tile.Equals(node.tile);
		}

		public override int GetHashCode()
		{
			return this.tile.GetHashCode();
		}
	}

	#endregion
}

Exemplo de Uso

star = new AStar<GameObject>();

// Dificuldade de andar de um gameobject para outro.
// A dificuldade pode ser maior ou menor se estiver andando num pântano, ou
// se a preferência for andar em diagonal, ou se o destino for uma montanha,  etc
star.DifficultToWalkDelegate = delegate(GameObject from, GameObject to) {
      return 1;
};

// Verifica se o gameobject é "andável". Pode ser que esteja temporariamente bloqueado,
// ou que um inimigo esteja em cima daquele gameobject específico.
star.IsWalkableDelegate = delegate(GameObject t) {
      return t.tag != "blocked";
};

// Um chute educado da possível  distância de um gameobject até o outro.
// Aqui usamos a distância em linha reta de um até o outro.
star.HeuristicDistanceDelegate = delegate(GameObject from, GameObject to) {
      return Vector3.Distance(from.transform.position, to.transform.position);
};

// Consulta sobre os vizinhos do gameobject.
// Podemos ter vários tipos de configuração diferente.
// Os vizinhos, em um grid 2d por exemplo, podem ser os 4 (de cima, de baixo,
// da esquerda e da direita) ou até 8 (contando com as diagonais).
star.GetNeighboorDelegate = delegate(GameObject t) {
      int index = int.Parse(t.name);
      return neighboorsOf(index);
};

Espero que seja útil =)

No meu repositório, entre outras coisas, tem o código disponível: https://gitlab.com/z3r0_th/unity-utilities

 

Unity3d AStar Pathfinding melhor caminho, exemplo com algoritmo

Achar melhor caminho com algoritmo A* exemplo

Debug Color

Caso ainda não saiba é possível trocar a cor que aparece na janela de debug da Unity. É bem Simples, basta usar a tag ‘color’ e o nome da cor:

Debug.Log("<color=red>" + "errorMessage" + "</color>");
Debug.Log("<color=green>" + "goodMessage" + "</color>");

Caso queira usar marcação em Hexadecimal, como em tags html, também é possível:

Debug.Log("<color=#FF0000>" + "errorMessage" + "</color>");
Debug.Log("<color=#00FF00" + "goodMessage" + "</color>");

Claro, podemos escolher uma cor customizada:

public Color color;

public TestDebug() {
     Debug.Log("<color=" + ToHex(color) + ">" + "Message" + "</color>");
}

public string ToHex(Color c) {
     return string.Format("#{0:X2}{1:X2}{2:X2}", (int)(c.r * 255), (int)(c.g * 255), (int)(c.b * 255));
}

Assim podemos deixar o console do Debug mais legível e interessante 😉

Screen Shot 2016-03-02 at 10.20.45 PM

Debug em Cores

Salvar para arquivo com Serialization C#

Serialização é o processo de converter um objeto em uma Representação de dados. Com essa representação pode-se, por exemplo, salvar em um arquivo no disco, num banco de dados, enviar para um serviço online, ou trocar informação com outras aplicações, etc. O objetivo principal é Salvar o estado do objeto de modo a recuperá-lo posteriormente.

Para serializar um objeto em C# (Para o formato Binário ao menos) precisamos marcar a classe com o atributo Serializable. Note que as classes marcadas como Serializable não poderão ser estendidas. Uma exceção é lançada no caso de tentar serializar um objeto de alguma classe que não foi marcada como Serializable.

Por padrão todos os campos públicos serão serializados – Ou seja, se houver outros objetos na classe estes serão serializados em cascata. Para evitar que algum campo seja serializado indevidamente marcamos este campo com o atributo NonSerializedAttribute.

Caso seja necessário podemos customizar a serialização de nossas classes, tomando controle dos passos para a serialização. A interface ISerializable é usado para customizar a serialização. No exemplo a seguir veremos apenas a serialização básica.

Exemplo

Considere que estamos fazendo um jogo bem simples e gostaríamos de salvar em arquivo algumas informações sobre o progresso do jogador: Pontuação, vidas restantes, level do jogador e nome. Além disso gostaríamos de saber a construção do Personagem: Arma, Armadura, Capacete, e uma lista de 5 itens:

//Alguns Enum para facilitar a atribuição dos itens do jogador nesse exemplo
public enum ArmorClass {
     High,
     Medium,
     Low
}

public enum Item {
     Helth,
     Mana,
     Teleport
}
[System.Serializable]
public class Game {
     public float score;
     public int lifes;
     public string name;
}

[System.Serializable]
public class Player {
     public ArmorClass helmet;
     public ArmorClass armor;
     public ArmorClass sword;
     public Item [] itens;
}

Note que como as classes são “Serializable” se declaramos ‘public’ dentro de um Monobehaviour na Unity, elas serão editáveis. Bom, para salvar o progresso do jogador e suas opções de Personagem precisamos de uma estrutura que mantenha o que precisamos que seja salvo. Algo como:

[System.Serializable]
     public struct GameProgress {
     public Game game;
     public Player player;
     public int saveIndex;
}

Precisamos importar algums namespaces:

using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

Podemos agora criar os métodos de salvar e carregar a nossa estrutura. Utilizaremos o BinnaryFormatter e salvaremos para um arquivo:

 

 

public class SerializationTest : MonoBehaviour {

   //Nós podemos editar estas variávels no Editor
   public Game game;
   public Player player;
   public int index;

   //Se pressionarmos 'A' salvamos o estado atual
   //Se pressionarmos 'B' carregamos o estado salvo
   void Update() {
      if (Input.GetKeyDown(KeyCode.A)) {
         Save();
      }

      if (Input.GetKeyDown(KeyCode.B)) {
         Load();
      }
   }

   //Salva. Note que não estamos, mas deveríamos, tratar exceções
   void Save() {
      //Criamos a estrutura que mantém referência dos elementos de interesse.
      GameProgress progress = new GameProgress();
      progress.game = game;
      progress.player = player;
      progress.saveIndex = index;

      //Salvamos para o arquivo DataFile.dat - Mas a extensão e local pode ser qualquer um.
      FileStream fs = new FileStream("DataFile.dat", FileMode.Create);
      BinaryFormatter serializer = new BinaryFormatter();
      serializer.Serialize(fs, progress);
      fs.Close();

      Debug.Log("Saved");
   }

   //Carrega. Note que não estamos, mas deveríamos, tratar exceções
   void Load() {
      //Carregamos o arquivo salvo.
      FileStream fs = new FileStream("DataFile.dat", FileMode.Open);
      BinaryFormatter formatter = new BinaryFormatter();

      //Desserializamos e carregamos o objeto na estrutura 'GameProgress'
      GameProgress progress = (GameProgress) formatter.Deserialize(fs);
      game = progress.game;
      player = progress.player;
      index = progress.saveIndex;

      Debug.Log("Loaded");
   }
}

Serialization Exemplo Editor Unity3D

Serialization

Note que para salvar estruturas da Unity como GameObject, Collider e outros é um pouco mais complicado – Porque tem construtores e padrões específicos que somente a serialização básica do C# não daria conta. Podemos utilizar estruturas intermediárias para salvar esse tipo de objeto. Inclusive fazer uso da nova classe auxiliar JSONSerialization da Unity, que podemos abordar num post futuro.

PlayerPrefs Unity3D

PlayerPrefs Unity3D

O quê é?

PlayerPrefs é uma Classe auxiliar da Unity3D que serve para salvar preferencias do Jogador entre sessões do jogo. É muito útil para salvar pequenas configurações e situações como: Som habilitado ou não, língua, Fullscreen ou Window, ultimo checkpoint ou ultimo jogo salvo (não o save do estado do game, mas o nome do arquivo que contém essas informações), entre outros ajustes. Apesar de não ser para o propósito de “Salvar” o jogo, muitos desenvolvedores utilizam, pela comodidade, PlayerPrefs para salvar o progresso do jogo.

Segurança e Localização

As variáveis não são salvas com segurança – Não são nem criptografadas nem protegidas de acesso (Se necessário, isso fica a cargo do desenvolvedor). Na verdade é possível modificá-las fora da aplicação, abrindo e editando o arquivo, ou acessando o registro do Windows. Caso tenha curiosidade, os arquivos podem ser encontrados em (Referencia: link1, link2):

  • Mac: ~/Library/Preferences/unity.[company name].[product name].plist
  • Windows: No Registro (cmd – comando register). HKCU\Software\[company name]\[product name]
  • Windows App Store: %userprofile%\AppData\Local\Packages\[ProductPackageId]>\LocalState\playerprefs.dat
  • Linux: ~/.config/unity3d/[CompanyName]/[ProductName]
  • Windows Phone 8: Em um folder local da própria aplicação
  • iOS: /Apps/ your app’s folder /Library/Preferences/ your app’s .plist
  • Android: /data/data/appname/shared_prefs/appname.xml

Tipos  de variáveis

Com PlayerPrefs é possível salvar os seguintes tipos de variáveis: Float, Int e String. 

Caso queira salvar qualquer outro tipo de variável o melhor a fazer é, ou compor entre esses 3 tipos, ou serializar e converter para uma string. O Bool é convenção utilizar o inteiro, onde 0 é false e 1 é true.

Valores Padrão

Para cada tipo de variável (Float, Int, String) há um método para Setar a variável (SetFloat, SetInt, SetString), onde passa-se a chave de acesso (uma string pela qual iremos acessar o valor) e o valor para aquela chave. Para obter a variável utilizamos o método Get (GetFloat, GetInt, GetString), onde passamos a chave e um valor default (que será retornado caso a chave não tenha sido “setada” anteriormente).

Tamanho

PlayerPrefs não foi concebido para suportar valores grandes. Para aplicações Web há um limite de 1Mb. Para outras plataformas não há limites impostos pela Unity, contudo há limites das próprias plataformas. No windows, por exemplo, um registro tem limite de 1Mb. Há relatos, inclusive, de crashs estranhos em outras plataformas conforme o tamanho aumenta. Portanto, o melhor é manter o tamanho Total abaixo de 1Mb para compatibilidade entre todas as plataformas.

Exemplos

Funções:

void SetFloat(string key, float value);
void SetInt(string key, int value);
void SetString(string key, string value);
float GetFloat(string key, float defaultValue);
int GetInt(string key, int defaultInt);
string GetString(string key, string defaultString);
bool HasKey(string key);
void DeleteKey(string key);
void DeleteAll();

Exemplo de uso

public class SaveProgress : Monobehaviour {
     public string PlayerName; //Jogador que está jogando atualmente
     public float PlayerScore; // Score do Jogador atual
     public int Life; // Quantas vidas faltam para o jogador atual
     public bool FullScreen; //Se é ou não fullScreen, preferencia global

     private string PlayerNameKey = "Player";
     private string PlayerScoreKey = "Score_";
     private string PlayerLifeKey = "Key_";
     private string FullScreenKey = "FullScreen";

     public void Save() {

          try {
               PlayerPrefs.SetString(PlayerNameKey, PlayerName);
               PlayerPrefs.SetFloat(PlayerScoreKey + PlayerName, PlayerScore);
               PlayerPrefs.SetInt(PlayerLifeKey + PlayerName, Life);
               PlayerPrefs.SetInt(FullScreenKey, BoolToInt(FullScreen));

          } catch(System.Exception e) {
               //Provavelmente excedeu o Limite de 1Mb no WebPlayer
               Debug.LogError(e.ToString());
          }
          PlayerPrefs.Save();
     }

     public void LoadLatest() {
          if (!PlayerPrefs.HasKey(PlayerNameKey)) {
               //Primeira vez jogando, cria um novo jogador
               CreatePlayer();
               return;
          }
          PlayerName = PlayerPrefs.GetString(PlayerNameKey); // Carrega informações do ultimo Jogador Jogado..
          PlayerScore = PlayerPrefs.GetFloat(PlayerScoreKey + PlayerName, 0); // Valor default é 0
          Life = PlayerPrefs.GetInt(PlayerLifeKey + PlayerName, 3); // Valor default é 3
          FullScreen = BoolFromInt(PlayerPrefs.GetInt(FullScreenKey, 0));
          Screen.fullScreen = FullScreen;
     }

     public void DeletePlayer(string PlayerName) {
          PlayerPrefs.DeleteKey(PlayerScoreKey + PlayerName);
          PlayerPrefs.DeleteKey(PlayerLifeKey + PlayerName);
          PlayerPrefs.DeleteKey(PlayerScoreKey + PlayerName);
          string CurrentPlayer = PlayerPrefs.GetString(PlayerNameKey);
          if (CurrentPlayer == PlayerName) {
               PlayerPrefs.DeleteKey(PlayerNameKey);
               //returna para menu
          }
     }

     public void Clear() {
           PlayerPrefs.DeleteAll();
     }

     private bool BoolFromInt(int n) {
          return n == 0 ? false : true;
     }

     private int BoolToInt(bool v) {
          return v ? 1 : 0;
     }
}