sexta-feira, 20 de junho de 2014

Tutorial – criando um jogo de plataforma demo com Pygame e Tiled

Desviando só um pouquinho do tema deste blog, gostaria de compartilhar este tutorial que montei.

1 - Introdução
Pygame é uma biblioteca da linguagem Python para desenvolvimento de games.
O download desta pode ser feito em: http://www.pygame.org/download.shtml
Neste tutorial, vamos utilizá-la para criar um jogo de plataforma básico (demo).
Além disso, vamos utilizar para a criação do mapa da tela, o software Tiled, e uma lib para manipular o arquivo do mapa criado por este.
O download do Tiled pode ser feito em: http://www.mapeditor.org
O arquivo do mapa gerado pelo Tiled, é um xml com uma extensão chamada tmx.
Existem algumas bibliotecas que foram criadas para manipular este tipo arquivo, e neste caso, escolhemos trabalhar com a biblioteca do Richard Jones. Para isso podemos baixar o arquivo tmx.py do link: https://github.com/renfredxh/tmx
  • Baixe e instale o Pygame e o Tiled. Após isto, crie um diretório chamado “demo“ e coloque dentro dele o arquivo tmx.py.
2 – Criação do mapa da tela
  • Abra a linha de comando do Linux e digite tiled para abrir o Tiled. Crie um novo arquivo e salve-o no diretório “demo”.
O Tiled possui dois tipos de camadas:
Camadas de tiles
Onde é desenhado o cenário do mapa através de imagens.
Por padrão, o mapa já virá com uma camada de tiles criada. Para adicionar imagens nesta camada, clique no menu Mapa > Novo Tileset e selecione a imagem desejada. Isso deve ser feito para cada imagem.
As imagens carregadas ficarão disponíveis na lista de tilesets à direita da tela. Ao clicar em uma imagem após carregada, você vai perceber que se passar o mouse em cima do mapa, ela estará disponível para ser carimbada nele conforme seus cliques. Além disso, você não precisa carimbar a imagem inteira. Você pode escolher quais tiles (pedacinhos) da imagem você quer selecionar.
  • Carregue as imagens desejadas e desenhe todo o mapa da tela do seu jogo.

Camada de objetos
Onde são adicionados os objetos que ocupam espaço físico ou representam triggers no mapa. Esta camada normalmente deve ficar invisível (para isso, basta desmarcar o checkbox dela).

  •  Para criar uma camada de objetos, clique no menu Camada > Adicionar camada de objetos, e renomeie a camada criada como “triggers”.
A camada de objetos é onde você define paredes, chão, teto, plataformas, indivíduos, itens etc.
Se você quer criar, por exemplo, uma parede lateral, você deve desenhá-la na camada de tiles, e na camada de abjetos adicionar um retângulo sobre ela.
Ou seja, o desenho em si não representará nada para o pygame a não ser uma imagem a ser renderizada, de modo que somente os objetos (alinhados com a imagem) poderão interagir com o código.
Os objetos dessa camada serão enxergados e manipulados pela biblioteca tmx.
  • Através do botão Inserir Objeto (O) no menu principal do topo, crie os objetos retangulares desejados que representem as paredes, teto, chão e plataformas do seu jogo. Clique com o botão direito em cada objeto, selecione Propriedades do objeto e insira uma nova propriedade com nome “blockers” e valor “tlrb”.
    Além dos objetos retangulares, você pode inserir ainda objetos carregados a partir de imagens externas. Isso serve para representar os personagens e itens da tela.
Todavia, não vamos carregar ainda a imagem diretamente no mapa. Vamos carregar uma imagem auxiliar onde cada quadradinho da imagem seja aproximadamente do tamanho de um tile (32x32 px).
Ex.:

Esses quadradinhos, de quantidade a definir, devem representar cada tipo de item, personagem ou trigger da tela.
  • Crie uma imagem auxiliar que represente os itens do seu jogo em um editor qualquer e clique em Mapa > Novo Tileset para adicioná-la.
  • Clique com o botão direito em cada tile do tileset e adicione uma propriedade que nomeie o item representado por cada tile. Ex.: nome “player” e value “yes”.
  • Clique na flechinha rosa abaixo da caixa de tilesets e exporte esse tileset para o diretório “demo” como .tsx.
  • Através do botão Inserir Tile (T) no menu principal do topo, coloque no mapa os objetos auxiliares desejados que representem indivíduos, itens ou triggers do seu jogo.

3 - Codificação
  • Importe as bibliotecas pygame e tmx.
  • Inicialize o pygame e o joystick:
pygame.init()
pygame.joystick.init()
  • Crie o objeto que representa a tela (passando como parâmetro o tamanho desta):    screen =   pygame.display.set_mode((768, 576))                             
  •  Defina um temporizador Para atualizar os frames da imagem a ser renderizada:    clock = pygame.time.Clock()
  •  Carregue o mapa criado no Tiled:
           tilemap = tmx.load('map.tmx', screen.get_size())

Sprites são a representação de indivíduos, itens etc no jogo, e devem ser criados para manipular os mesmos.
Ex.1 – criação de um sprite que representa o jogador (como este é apenas um único, deve-se escolher o índice 0 do array):
sprites = tmx.SpriteLayer()
start_cell = tilemap.layers['triggers'].find('player')[0]
player = Player((start_cell.px, start_cell.py), sprites)
tilemap.layers.append(sprites)
Ex.2 – criação do grupo de sprites de inimigos (como são vários, é necessário inserir em um loop) :
enemies = tmx.SpriteLayer()
for enemy in self.tilemap.layers['triggers'].find('enemy'):
    Enemy1((enemy.px, enemy.py), enemies)
tilemap.layers.append(enemies)
Podemos notar que: o nome “triggers” que é citado agora, é o mesmo que foi designado no mapa do Tiled para a camada de objetos; o nome passado como parâmetro para a função find, também foi designado no mapa do Tiled como propriedade de cada tile representando um item ou indivíduo; a função find retorna a posição do item no mapa em relação aos eixos x e y;
  • Crie grupos de sprites para todos os indivíduos e itens do seu jogo na tela do mapa criado. Ex.: player, enemies, bullets, guns, lifes etc.
O jogo precisa ter um loop principal que manterá o jogo rodando e sendo atualizado.
Neste loop, você deve:
- limitar os frames a serem atualizados por segundo: dt = clock.tick(30)
- capturar os eventos gerados no pygame com o sub loop:
                for event in pygame.event.get():
                    # fecha o jogo caso haja clique no “x” da janela
                    if event.type == pygame.QUIT:
                        return
           - preencher a tela com uma cor solida, de modo a evitar blurring dos itens renderizados:     
               screen.fill((250,250,250))
- atualizar o objeto do mapa conforme frames: tilemap.update(dt / 1000., self)
- desenhar os itens do objeto do mapa na tela: tilemap.draw(screen)
- atualizar a tela: pygame.display.flip()
- verificar a life do player de modo a tomar a ação adequada caso a life tenha acabado (como por exemplo sair do jogo ou apresentar uma mensagem de game over).
  • Crie um loop principal para o seu jogo.
Para todos os sprites criados anteriormente, é necessário ter uma classe com as definições dos mesmos.
O objeto tilemap deverá manipular os objetos das classes de cada sprite automaticamente, a partir de algumas funções e atributos padrão.

Ex.1 – Classe de indivíduos/itens

  • Crie a classe do sprite desejado passando a classe sprite como parâmetro: (pygame.sprite.Sprite).
  • Carregue a imagem do indivíduo/item no atributo image: image = pygame.image.load('...')
A função principal da classe deve ter o seguinte formato básico:
def __init__(self, location, *groups):
    super(NOME, self).__init__(*groups)
O parâmetro “location” se refere à posição inicial do indivíduo/item e o parâmetro “*groups” faz referência ao objeto de sprites localizado na chamada da classe.
A posição do indivíduo deve ser marcada no atributo “rect”.
Para inicializar o atributo de localização do indivíduo/item use:
self.rect = pygame.rect.Rect(location, self.image.get_size())
É necessário ter uma função chamada “update” para atualizar os objetos da classe:
def update(self, dt, game):
  •  Programe o incremento ou decremento do atributo “rect” para mover o indivíduo/item na horizontal, ex.: self.rect.x += 100 * dt
Neste caso o número 100 é o salto e dt é a ponderação pela velocidade de atualização de frames, configurada anteriormente.
  •  Verifique uma possível colisão do objeto atual com outros objetos da seguinte forma:
              for cell in game.tilemap.layers['triggers'].collide(self.rect, 'reverse'):
                  # aqui vão as ações cabíveis em caso de colisão.
No exemplo acima, estamos verificando a colisão do objeto atual com objetos do tipo 'reverse'. Lembrando que esses tipos são definidos nas propriedades do mapa criado no Tiled.
  •  Verifique se o objeto atual colidiu com o player:
              if self.rect.colliderect(game.player.rect):
                  # aqui vão as ações cabíveis em caso de colisão.

Ex.2 – Classe do player
A classe do player deve conter quase as mesmas coisas que a classe do exemplo anterior, pois ele também é um indivíduo/item.
Todavia, haverá provavelmente um conjunto maior de ações e verificações a serem feitas.
Exemplos:
Em descanso: self.resting = False
Velocidade: self.dy = 0
Marcação da life do player.
Leitura do joystick e teclado:
joystick = pygame.joystick.Joystick(0)
joystick.init()
key = pygame.key.get_pressed()
# anda para frente
if key[pygame.K_LEFT] or joystick.get_axis(0)<0:
    self.rect.x -= 300 * dt
# anda para trás
if key[pygame.K_RIGHT] or joystick.get_axis(0)>0:
    self.rect.x += 300 * dt
# pulo e queda
if self.resting and ( key[pygame.K_SPACE] or int(joystick.get_button(2)) == 1) :
    self.dy = -500
    self.dy = min(400, self.dy + 40)
    self.rect.y += self.dy * dt
Verificação de colisão com “blockers” (chão, parede ou plataformas):
new = self.rect
self.resting = False
for cell in game.tilemap.layers['triggers'].collide(new, 'blockers'):
    blockers = cell['blockers']
    # limite à direita
    if 'l' in blockers and last.right <= cell.left and new.right > cell.left:
        new.right = cell.left
    # limite à esquerda
    if 'r' in blockers and last.left >= cell.right and new.left < cell.right:
        new.left = cell.right
    # limite do chão
    if 't' in blockers and last.bottom <= cell.top and new.bottom > cell.top:
        self.resting = True
        new.bottom = cell.top
        self.dy = 0
    # limite do teto
    if 'b' in blockers and last.top >= cell.bottom and new.top < cell.bottom:
        new.top = cell.bottom
        self.dy = 0

É importante forçar que a janela mantenha o foco no player como item central: game.tilemap.set_focus(new.x, new.y)
  • Crie as classes dos indivíduos e itens do seu jogo.

Um vídeo tutorial de Richard Jones (mais detalhado, porém em inglês) está disponível no link: http://pyvideo.org/video/2620/introduction-to-game-programming

Abraços miningnoobs !

quinta-feira, 5 de junho de 2014

Validação de modelos


Conforme já mencionamos em outro post, uma das etapas mais importantes no processo de desenvolvimento de um modelo é sua validação.
Isso porque não adianta você criar um modelo, e partir do pressuposto de que ele funciona. É preciso verificar e provar tal pressuposto, de forma a validá-lo.

Já comentamos, também em outro post, que no processo de modelagem, provavelmente vamos extrair uma amostra de dados da população total.
Essa amostra, entretanto, não deverá ser utilizada inteiramente para fomentar o aprendizado do modelo. Isso porque uma parte desta deverá ser reservada para a etapa de validação, ou teste.

O motivo disso é simples. É fácil dizer que o modelo acerta suas previsões e classificações para os mesmos indivíduos que fizeram parte do processo de aprendizado. Afinal de contas, o modelo já conhece esses indivíduos. Entretanto, na prática da "vida real do modelo", ele será utilizado para prever e classificar indivíduos que ele desconhece. Esse é o verdadeiro desafio, e portanto, é isso que deve ser medido na validação.

Pra tal, precisamos reservar uma parcela da nossa amostra, que não tenha nenhuma participação no processo de desenvolvimento do modelo, e que, apenas no final seja utilizada para colocá-lo à prova.
O acerto nesta amostra de indivíduos (amostra de validação ou teste), poderá ser considerado o acerto previsto final esperado na prática.

O tamanho da amostra de validação pode variar dependendo do caso, mas algo em terno de 30% da amostra total é bastante razoável.

É bom ressaltar que neste caso estamos falando de aprendizado supervisionado. E é justamente neste momento de validar, que estamos supervisionando nosso aprendizado.


Por exemplo:


Imaginemos que nosso público alvo são homens acima de 50 anos em São paulo, e que conseguimos coletar uma amostra de 100.000 indivíduos.
Vamos reservar 30% destes para validação (amostra de validação), sendo 30.000 indivíduos, e os outros 70% (70.000 indivíduos) serão utilizados na aprendizagem (amostra de desenvolvimento) do modelo. É importante que essa separação de indivíduos seja aleatória.

Agora vamos imaginar que criamos um modelo de propensão para uma resposta binária (como infarto sim/não) com a amostra de desenvolvimento, e aplicamos o modelo na amostra de validação para testar seu acerto.
Nesse exemplo hipotético, digamos que nosso acerto tenha sido de 75%.
Isso significa que quando estivermos usando o modelo na sua aplicação prática, ele tenderá a acertar em  aproximadamente 75% de suas previsões. Essa é a capacidade de generalização dele.

Um ponto importante de comentar é que, um modelo não pretende acertar todos os casos, pois dificilmente isso é viável. Um modelo se propõe a acertar uma quantidade razoável, que seja melhor do que uma escolha aleatória, portanto este deve fornecer uma medida para sua expectativa de acerto. Devemos ter em mente que em meio a muitos acertos, sempre haverão erros, mas isso ainda será melhor do que tomar uma decisão de forma arbitrária.

Quando temos uma amostra pequena, ou simplesmente quando queremos apenas garantir um resultado mais estável, podemos usar técnicas de validação que repetem esse processo de separação de amostras várias vezes para garantir expectativas de acerto mais seguras, como K-Folds,  mas isso já é outro assunto.

Acertos nas categorias


Cuidado! Vamos agora imaginar que na amostra do nosso público alvo, apenas 10% dos indivíduos tiveram infarto no período do estudo, e os outros 90% não.
Se nosso modelo simplesmente atribuir uma previsão negativa para todos os indivíduos, ele acertará 90%. Esse número parece muito bom olhando o acerto total, mas na prática, sabemos que o modelo é inútil, pois não acerta nenhum caso onde o indivíduo é positivo.
Deviso a isso, não podemos olhar apenas o acerto total. Precisamos avaliar o acerto de todas as categorias, para garantir que mesmo em amostras desbalanceadas, o acerto será razoável para todas as categorias de resposta.
Dessa forma, poderemos dizer que nosso modelo é capaz de discriminar entre as estas (neste caso, infarto sim/não).


Abraços miningnoobs!