Flixel é uma game engine escrita em ActionScript 3, facilita bastante a construção de alguns tipos e formatos de jogos, especialmente jogos de plataforma. Já possuí classes e métodos para lidar com:
Texto;
Colisão;
Imagens
Efeitos
Mapa;
Pontos;
Estados;
Fases;
Inimigos;
Vidas;
Câmera
e muitas outras coisas.
Esse post tem o objetivo de construir um Hello World, esse exemplo será o passo inicial para a construção de jogos mais complexos no futuro.
Em primeiro lugar você precisa conhecer o site do Flixel: http://www.flixel.org lá você encontra a game engine para download, fórum, wiki e outros games já desenvolvidos pela comunidade.
Para construir o jogo nós precisamos do Flex SDK, que são as bibliotecas padrões do ActionScript 3, a game engine Flixel utiliza essas bibliotecas internamente e você provavelmente vai precisar de alguma delas para as customizações ou necessidades do seu jogo.
Faça o download: http://opensource.adobe.com/wiki/display/flexsdk/download?build=4.1.0.16248&pkgtype=1 e descompacte em algum diretório da sua preferência, daqui a pouco nós vamos usar ele.
Terceiro passo: Download do Flixel
Vamos precisar dele para a construção do jogo, ele na verdade é um framework de desenvolvimento de jogos. Já possui muitas bibliotecas prontas que facilitam e aceleram o desenvolvimento. Faça o download em: http://github.com/AdamAtomic/flixel/archives/master e descompacte também em algum diretório da sua preferência.
Criando um Novo Projeto
Abra o FlashDevelop;
Clique no menu “Project”;
Clique na opção “New Project”;
Em “Installed Templates” selecione “AS3 Project”;
Em “Name:” digite HelloWorld;
Marque o checkbox na opção “Create directory for project”;
Clique no botão “Ok”.
Configurando o Flex SDK e Flixel no Projeto
Clique no menu “Project”;
Clique na opção “Properties…”;
Clique na aba “Compiler Options”;
No campo “Custom Path to Flex SDK” clique no botão “…” e selecione o diretório onde você descompactou o Flex SDK;
Clique na aba “Classpaths”;
Clique no botão “Add Classpaths…” e selecione o diretório onde você descompactou o Flixel, mas você precisa entrar na pasta e selecionar a pasta “org”;
Clique no botão “Ok”.
Programando o HelloWorld
HelloWorld.as
No diretório src do projeto existe o arquivo Main.as, apague esse arquivo;
Clique com o botão direito do mouse na pasta “src” depois “add” e “New Class…“;
Digite para o campo “Name:” o valor: HelloWorld.
Será gerando um código inicial como esse:
package{/**
* ...
* @author Patrick Espake
*/publicclass HelloWorld
{public function HelloWorld(){}}}
Vamos alterar para ficar dessa forma:
package{// Importa a game engine Flixelimportorg.flixel.*;// Tamanho da tela e cor de fundo[SWF(width ="640", height ="480", backgroundColor ="#000000")]// Classe HelloWorld// herda a classe FlxGame para incorporar as funcionalidades do Flixelpublicclass HelloWorld extends FlxGame
{// Método construtorpublic function HelloWorld(){// Tamanho da área útil// classe inicial a ser chamada depois do jogo carregar// e nível de zoomsuper(640, 480, PlayState, 1);}}}
Através dos comentários acima você deve ser capaz de entender a maioria do código, basicamente nós definimos a dimensões do swf que será gerado, a cor de fundo, e qual classe de estado deve ser chamada após o jogo terminar de carregar, nesse exemplo PlayState que não criamos ainda.
E importante notar que nós herdamos no código a classe FlxGame, através desse herança podemos adquirir métodos e atributos da game engine Flixel.
Agora precisamos dizer para o FlashDevelop sempre compilar essa classe que acabamos de criar, clique com o botão direito do mouse sobre a classe “HelloWorld.as” e clique na opção “Always Compile“.
PlayState.as
Crie uma nova classe no diretório src chamada PlayState.
Altere o seu código para ficar da seguinte forma:
package{// Importa a game engine Flixelimportorg.flixel.*;// Classe inicial do jogo// herda de FlxStatepublicclass PlayState extends FlxState
{// Método do Flixel// usado para criar os elementos do jogo
override public function create():void{// Criação do texto com o Hello World!
var t1:FlxText =new FlxText(0, 0, FlxG.width, "Hello World!");
t1.alignment="center";
t1.size=48;// Adiciona o texto na tela
add(t1);}}}
A classe PlayState representa o estado de jogo do Flixel, a game engine possui uma máquina de estados, você pode ter vários estados, por exemplo: menu, créditos, primeira fase, segunda fase, game over e vitória. O Flixel já possui alguns métodos para realizar facilmente essa transição de estados.
Nessa classe nós herdamos de FlxState que possui vários métodos e atributos para lidar com estados do jogo, através do método create é criado os elementos do jogo, nesse exemplo simples apenas um texto com “Hello World!”.
Você pode pressionar a tecla F5 para testar, caso você tenha o Flash Debugger instalado ele já vai abrir a tela com o jogo, caso contrário você terá que ir na pasta bin e abrir o arquinvo index.html no navegador.
Nas últimas semanas eu veio estudando a fundo a técnica de inteligência artificial de redes neurais. Não é toa que praticamente os meus dois últimos posts foram sobre esse assunto.
Tudo começou com a ideia de criar um agente inteligente que conseguisse jogar Mario sozinho, a minha pretensão nunca foi muito grande, eu apenas tinha a ideia de fazer algo simples que funcionasse, mas não tinha a grande pretensão de fazer um super agente inteligente capaz de passar por vários níveis diferentes de dificuldade do jogo Mario. Partindo disso, com o único objetivo de aprendizado, eu decidi usar a técnica de redes neurais e fazer o Mario passar pela primeira fase.
Para simplificar mais ainda, eu fiz por padrão o Mario andar para a direita e a única ação que é aplicada inteligência artificial é a decisão se o Mario deve ou não pular.
O código do jogo Mario eu obtive do site: http://www.marioai.org/gameplay-track/getting-started, esse é o site da competição que tem o foco na criação de agentes inteligentes e criação de fases para o Mario, usando inteligência artificial. Eles não disponibilizam o código da parte de inteligência artificial, cabe a cada participante implementar o seu algoritmo e enviar para a competição, o melhor agente inteligente vence. Eu também não tinha a ideia de vencer essa competição, não tenho nível e conhecimento suficiente para isso ainda.
Antes de entrarmos em código e implementação, é importante entender do que consiste a técnica de redes neurais.
De forma resumida, uma rede neural é constituída por um neurônio (perceptron), que possui a seguinte estrutra:
Dendritos;
Corpo celular;
Axônio.
O dendrito(s) corresponde(m) as entradas (inputs) do neurônio, o corpo celular é responsável pelo processamento e o axônio representa a saída (output).
O algoritmo de uma rede neural se resume em:
Os dendritos recebem um valor de entrada cada um, esse valor é multiplicado por um peso, uma soma ponderada na verdade;
O neurônio, corpo celular, recebe a soma ponderada e através de uma verificação matemática, decide o valor de saída para o neurônio. Isso é conhecido por função de ativação, que em muitas vezes se resume em um if que testa se a soma é maior ou menor que zero;
O axônio transmite essa saída
Você pode pensar “Hum muito bem, onde está a inteligência artificial nisso?”, a inteligência artificial está no próximo passo do algoritmo, o neurônio deve ser treinado, ele deve receber um conjunto de valores de entrada para os dendritos e a saída válida, saída conhecida para esse conjunto de entradas. Com posse disso o neurônio é treinado, e o grande segredo de uma rede neural é o ajuste dos pesos que são multiplicados pelas entradas no dendritos, nessa parte que consta a inteligência artificial. O grande foco e obter os pesos ideias para que a rede neural se comporte da melhor forma, depois de encontrar esses pesos basta em usá-los sempre.
Na verdade tudo é matemática.
Agora que está claro como tudo acontece vamos ver a implementação. Vamos começar pelas estruturas de dados mais básicas e avançamos para as mais complexas.
Dendrito
Dendrite.java
packagebr.pucpr.neuralnetwork;// Classe que representa um dendritopublicclass Dendrite {// Valor para o dendritoprivateint value;// Método construtor// aceita como parâmetro:// * value = valor para o dendritopublic Dendrite(int value){this.value= value;}// Obtém o valor atual do dendritopublicint getValue(){return value;}}
Axônio
Axon.java
packagebr.pucpr.neuralnetwork;// Classe que representa um axôniopublicclass Axon {// Sinal de saídaprivateint sign;// Obtém o sinal do axôniopublicint getSign(){return sign;}// Define o sinal para o axôniopublicvoid setSign(int sign){this.sign= sign;}// Transforma o sinal em um valor booleanopublicboolean signToBoolean(){if(sign ==-1){returnfalse;}returntrue;}}
Neurônio
Neuron.java
packagebr.pucpr.neuralnetwork;// Classe que representa um neurôniopublicclass Neuron {// Dendritosprivate Dendrite[] dendrites;// Axônioprivate Axon axon;// Pesosprivatefloat[] weights;// Pesos padrõesprivatefloat[] defaultWeights;// Tipo da função de ativaçãoprivateString activationType;// Constante de aprendizadoprivatefloat constantLearning;// Método construtor// aceita como pârametros:// * numberDendrites = Número de dendritos// * defaultWeights = Pesos padrão// * activationType = tipo da função de ativaçãopublic Neuron(int numberDendrites, float[] defaultWeights, String activationType){// Define os pesos padrõesthis.defaultWeights= defaultWeights;// Se não for definida o tipo da função de ativação,// então define como signif(activationType ==null){this.activationType="sign";}// Pesos
weights =newfloat[numberDendrites];// Dendritos
dendrites =new Dendrite[numberDendrites];// Cria valores iniciais randômicos para os pesosfor(int i =0; i < weights.length; i++){
weights[i]=Util.random(-1, 1);}// Constante de aprendizado
constantLearning =(float)0.0001;// Axônio
axon =new Axon();}// Soma os dendritos com os seus pesos// média ponderadapublicfloat sumDendrites(){float sum =0;if(defaultWeights !=null&& defaultWeights.length>0){for(int j =0; j < defaultWeights.length; j++){
weights[j]= defaultWeights[j];}}for(int i =0; i < weights.length; i++){
sum += dendrites[i].getValue()* weights[i];}return sum;}// Método de ativação// dependendo da soma e do tipo de ativação// envia o sinal -1 ou 1 para o axôniopublicvoid activation(){float sum = sumDendrites();
axon.setSign(1);if(sum <0){
axon.setSign(-1);}}// Treina o neurônio, afim de obter os melhores pesos para os dendritos// aceita como parâmetros:// * numberTimes = número de épocas// * percentageCorrect = percentual correto para o treinamento ser considerado bem sucedido// * examplesTraining = exemplos para o treinamentopublicvoid train(int numberTimes, int percentageCorrect, Point[] examplesTraining){System.out.println("Treinamento iniciado...");// Quantidade de treinamento verificadoint totalVerified =0;// Quantidade de treinamento verificado erradoint totalVerifiedWrong =0;// Quantidade de treinamento verificado corretoint totalVerifiedCorrect =0;// Repete até o número de épocas ser satisfeitofor(int i =0; i < numberTimes; i++){// Percorre todos os exemplos de treinamentofor(int j =0; j < examplesTraining.length; j++){// Obtém a saída adivinhada pelo neurônioint result = guess(examplesTraining[j]);// Calcula o fator de mudança do peso baseado no erro// erro = saída desejada - saída adivinhada// multiplica pela constante de aprendizadofloat weightChange = constantLearning *(examplesTraining[j].getOutput()- result);// Ajusta os pesos baseado no fator de mudança * a entradafor(int k =0; k < weights.length; k++){
weights[k]+= weightChange * examplesTraining[j].getVals()[k];}// Contabiliza se o valor adivinhado foi correto ou nãoif(result == examplesTraining[j].getOutput()){
totalVerifiedCorrect++;}else{
totalVerifiedWrong++;}}}// Quantidade de treinamentos verificados
totalVerified = totalVerifiedWrong + totalVerifiedCorrect;float totalPercentageCorrect =((100* totalVerifiedCorrect )/ totalVerified);System.out.println("Treinamento terminado...");System.out.println("Resultados obtidos:");System.out.println(" - Número de épocas executadas: "+ numberTimes);String indicativeSuccess ="bem sucedido";if(totalPercentageCorrect < percentageCorrect){
indicativeSuccess ="mal sucedido";}System.out.println(" - Indicativo de sucesso: Treinamento "+ indicativeSuccess);System.out.println(" - Porcentagem correto: "+ totalPercentageCorrect);System.out.println(" - Pesos:");if(defaultWeights !=null&& defaultWeights.length>0){for(int j =0; j < defaultWeights.length; j++){
weights[j]= defaultWeights[j];}}for(int i =0; i < weights.length; i++){System.out.println(" - Peso "+ i +": "+ weights[i]);}}// Método que adivinha a saída esperada// baseada nas entradas, de acordo com os pesos// calculadospublicint guess(Point point){// Define os valores para os dendritos
setDendrites(point.getVals());// Faz a ativação
activation();// Sinal de ativaçãoreturn axon.getSign();}// Define os valores para os dendritos// de acordo com um array de valorespublicvoid setDendrites(int[] vals){for(int i =0; i < vals.length; i++){
dendrites[i]=new Dendrite(vals[i]);}}// Obtém a constante de aprendizadopublicfloat getConstantLearning(){return constantLearning;}// Define uma nova constante de aprendizadopublicvoid setConstantLearning(float constantLearning){this.constantLearning= constantLearning;}// Obtém o axôniopublic Axon getAxon(){return axon;}// Obtém o tipo da função de ativação usadapublicString getActivationType(){return activationType;}}
Ponto
Point.java
packagebr.pucpr.neuralnetwork;// Classe que representa um ponto de treinamentopublicclassPoint{// Valores de entradaprivateint[] vals;// Saída esperadaprivateint output;// Método construtor// aceita os parâmetros:// * obs1 = tipo de obstáculo na posição 1// * obs2 = tipo de obstáculo na posição 2// * output = saída esperadapublicPoint(int obs1, int obs2, int output){
vals =newint[3];
vals[0]= obs1;
vals[1]= obs2;// Bias para quando as entradas forem zero
vals[2]=1;this.output= output;}// Valores de entradapublicint[] getVals(){return vals;}// Saída esperadapublicint getOutput(){return output;}}
Agente Inteligente
NeuralNetworkAgent.java
packagech.idsia.ai.agents.controllers;importbr.pucpr.neuralnetwork.Neuron;importbr.pucpr.neuralnetwork.Point;importch.idsia.ai.agents.Agent;importch.idsia.mario.engine.sprites.Mario;importch.idsia.mario.environments.Environment;// Classe que representa um agente inteligente que utiliza rede neuralpublicclass NeuralNetworkAgent extends BasicAIAgent implements Agent {// Neurônioprivate Neuron neuron;// Pontos para treinamentoprivatePoint[] examplesTraining;public NeuralNetworkAgent(){super("NeuralNetworkAgent");// Instância um neurônio// Com 3 dendritos, obstáculo 1, obstáculo 2 e // bias para quando as primeiras entradas forem zero
neuron =new Neuron(3, null, "sign");// Com pesos padrões// float[] defaultWeights = new float[3];// defaultWeights[0] = (float)0.12533271;// defaultWeights[1] = (float)-0.15752053;// defaultWeights[2] = (float)-0.22475332;// neuron = new Neuron(3, defaultWeights, "sign");// Cria os pontos para o treinamento
setExamplesTraining();// Realiza o treinamento
neuron.train(300, 43, examplesTraining);
reset();}publicvoid reset(){
action =newboolean[Environment.numberOfButtons];
action[Mario.KEY_RIGHT]=true;}publicboolean[] getAction(){// Através dos pesos calculados no treinamento// tenta jogar Mario sozinho// Define o valor para os dendritos baseado nos obstáculos do jogoint[] vals =newint[2];
vals[0]= mergedObservation[11][13];
vals[1]= mergedObservation[11][12];
neuron.setDendrites(vals);// Realiza ativação do neurônio, afim de obter o sinal de saída
neuron.activation();// De acordo com o neurônio faz o Mario pular ou nãoif(neuron.getAxon().signToBoolean()){if(isMarioAbleToJump){
action[Mario.KEY_JUMP]=true;}}else{
action[Mario.KEY_JUMP]=false;}return action;}// Cria os pontos para o treinamentopublicvoid setExamplesTraining(){
examplesTraining =newPoint[6];
examplesTraining[0]=newPoint(0, 0, -1);
examplesTraining[1]=newPoint(-10, 0, 1);
examplesTraining[2]=newPoint(20, 0, 1);
examplesTraining[3]=newPoint(-10, -10, 1);
examplesTraining[4]=newPoint(2, 0, 1);
examplesTraining[5]=newPoint(-11, 0, 1);}}
Main
Main.java
packagech.idsia.scenarios;importch.idsia.ai.agents.Agent;importch.idsia.ai.agents.controllers.NeuralNetworkAgent;importch.idsia.maibe.tasks.BasicTask;importch.idsia.mario.environments.Environment;importch.idsia.mario.environments.MarioEnvironment;importch.idsia.tools.CmdLineOptions;/**
* Created by IntelliJ IDEA. User: Sergey Karakovskiy, sergey at idsia dot ch Date: Mar 17, 2010 Time: 8:28:00 AM
* Package: ch.idsia.scenarios
*/publicclass Main
{publicstaticvoid main(String[] args){// final String argsString = "-vis on";// args = argsString.split("\\s");final CmdLineOptions cmdLineOptions =new CmdLineOptions(args);finalEnvironment environment =new MarioEnvironment();// final Agent agent = new ForwardAgent();// final Agent agent = cmdLineOptions.getAgent();final Agent agent =new NeuralNetworkAgent();// final Agent a = AgentsPool.load("ch.idsia.controllers.agents.controllers.ForwardJumpingAgent");final BasicTask basicTask =new BasicTask(environment, agent);
basicTask.reset(cmdLineOptions);
basicTask.runOneEpisode();System.out.println("cmdLineOptions.getLevelLength() = "+ cmdLineOptions.getLevelLength());System.out.println(environment.getEvaluationInfoAsString());System.exit(0);}}
Vamos entender como as classes acima se juntam para implementar a rede neural, a classe NeuralNetworkAgent.java possui os exemplos de treinamento, esses exemplos foram captados quando eu estava realmente jogando, por isso que a saída é conhecida, é o que se espera que o Mario faça quando ele tiver obstáculos pela frente. Além dos exemplos essa classe define o comportamento inicial do Mario que é andar para direita, a classe também realiza o treinamento, e com posse dos pesos ela abre a tela do jogo e coloca o Mario para jogar sozinho com inteligência. No código incluído está para toda vez os pesos serem calculados, é possível passar pesos padrões para o neurônio, essa parte do código está comentada, se você descomentar já irá ver o Mario jogando com os melhores pesos que eu encontrei.
A classe Point.java descreve os pontos ou ponto de entrada, ela recebe a informação se existem obstáculos na posição 1 e na posição 2 e a saída que deve existir.
A classe Dendrite.java descreve um dendrito e o seu valor de entrada.
A classe Axon.java descreve o axônio do neurônio, o sinal de saída, que também é transformado em um valor booleano.
A classe Neuron.java por sua vez que realiza todo o trabalho, cálculo dos melhores pesos e o emprego da inteligência artificial. Poder conter vários dendritos (várias entradas), uma única saída, os pesos para cada entrada e a constante de aprendizado. Inicialmente os pesos são valores randômicos, ao entrar no treinamento, os melhores pesos tentam ser encontrados, não sempre isso acontece, vai depender da quantidade de exemplos de treinamento e a quantidade de épocas. As épocas são quantas vezes o algoritmo vai ser repetido para tentar encontrar os melhores pesos para a rede neural. A constante de aprendizado é usada para alterar os pesos e tentar achar o melhor peso, as referências bibliográficas afirmam que quanto menor a constante de aprendizado, mais vai demorar para encontrar os pesos, mas os pesos encontrados são melhores e mais úteis para o funcionamento da rede.
A classe Main.java que executa o jogo e coloca todas as classes para trabalhar afim de obter a rede neural.
É claro que para o Mario rodar, é necessário ter toda o código fonte do Mario, disponibilizado no endereço que eu mencionei no começo do texto. Os códigos apresentados são apenas um agente inteligente que foi embutido no jogo do Mario.
Eu não vou entrar em mais detalhes do algoritmo, com posse dessas informações e comentários você já deve ser capaz de entender tudo que está acontecendo.
O que eu escrevi nesse texto não representa a verdade absoluta, eu ainda estou estudando o assunto, por isso algumas coisas podem estar erradas ou podem ser feitas de uma forma muito melhor. Apenas quero ajudar e propagar o conhecimento.