sábado, 28 de março de 2015

[Game Maker] Aula 3 - Configurando os inimigos

Nesta aula, eu irei mostrar como configurar, passo á passo, um inimigo simples, porém funcional. A característica do "inimigo" em todos os jogos é impedir o progresso do jogador com a inteligência artificial que lhe foi dada, criando um PvE.

Continuando a partir do que foi feito na aula anterior, desta vez vamos configurar os inimigos do jogo. O inimigo será muito simples: suas funções básicas serão avistar o jogador, perseguir o jogador, atacar o jogador caso tenha a oportunidade e também apanhar do jogador, até que venha a morrer.

Índice:
1. Preparações de sprites e objetos
2. Configurando o inimigo I
3. Configurando efeitos visuais
4. Configurando o inimigo II
5. Interação entre inimigo e personagem
6. Configurando o inimigo III
7. Morte, Game Over e outros detalhes
8. Finalização

1. Preparações de sprites e objetos
Primeiro, temos que criar outros sprites para os inimigos, neste caso, chamamos o primeiro de sprite_inimigo_menor. Você pode alterar este nome, como lhe convir melhor. Este primeiro sprite não será diferente de todos os outros: 16x16 e ponto de origem centralizado (x:8;y:8). O segundo, chamados de sprite_inimigo_grande. Como o próprio nome já diz, faremos um inimigo um pouco maior: 24x24, também com o ponto de origem da imagem centralizado (x:12;y:12). Você pode fazer uma animação básica em ambas as sprites se desejar, embora seja puramente cosmético.

Como fizemos um inimigo maior que a nossa Mask padrão (16x16), criaremos então um novo sprite para servir de Mask para ele. Faça como a mask anterior: uma imagem de tamanho 24x24, com um círculo redondo cobrindo a imagem o máximo possível, com a origem centralizada, chamado de sprite_mask_maior24x24. Desta maneira:

Agora, vamos criar objetos para os inimigos, porém vamos fazer isso de uma maneira um pouco diferente do que estávamos fazendo anteriormente. Vamos fazer o uso do recurso Parent.

Crie um objeto. Independente do nome que você deu os sprites, o nome do objeto deve ser objeto_inimigo_geral. Agora crie outros dois objetos, agora com o nome em relação aos sprites que você deu, neste caso, objeto_inimigo_menor e objeto_inimigo_maior. Em ambos, coloque as seguintes características: Solid, coloque suas sprites e masks correspondentes e por fim, seu Parent deve ser o objeto_inimigo_geral. Algo parecido com isso:

Agora que o terreno está pronto, vamos para a parte do GML.
_____________________________________________________

Antes, deixe-me dar uma breve explicação do que ocorreu aqui: nós colocamos o Parent do objeto_inimigo_menor e objeto_inimigo_maior como objeto_inimigo_geral. Ou seja, todas as alterações e tudo o que diz respeito ao objeto_inimigo_geral afetará diretamente os outros dois. Por exemplo: se quiséssemos destruir todas as instâncias do objeto_inimigo_menor e objeto_inimigo_maior, não teríamos que fazer duas ações: basta destruir todos os objeto_inimigo_geral, e seus representantes serão destruídos por consequência.

Neste caso, tudo o que estiver interligado com o objeto_inimigo_geral ou com os outros dois será considerado para o jogo um inimigo. A grande vantagem disso é que só iremos configurar os inimigos uma única vez e depois faremos pequenas alterações entre um e outro para darmos á cada um características mais únicas.

Se não tivéssemos feito isso, teríamos que copiar e colar o mesmo código em todos os inimigos do jogo e, caso algo tivesse dado errado ou quiséssemos fazer uma alteração, teríamos que fazer de um em um, pacientemente. Vale lembrar que uma instância do objeto_inimigo_geral nunca vai entrar diretamente no jogo. Ele só irá participar indiretamente, através dos seus "representantes".

Para que um "filho" faça exatamente a mesma coisa que seu pai em um determinado evento, existem duas opções: deixar o evento intocável no representante, ou então usar o a função "event_inherited();". A função "event_inherited();" basicamente copia o código do evento que ela foi adicionada do objeto-pai para o objeto-representante. Não se preocupe em entender isso agora, logo iremos trabalhar encima deste conceito e tudo ficará mais claro.
_____________________________________________________

Também é interessante colocar o Parent do objeto_inimigo_geral como objeto_obstaculo_geral, pois assim o inimigo também funcionará como um obstáculo comum qualquer, que o jogador não possa atravessar.

2. Configurando o inimigo I
Certo! Agora, vamos mexer nas características do objeto_inimigo_geral. Não precisamos alterar nada á respeito de sprite, nem solid, nem mask, nem visible, etc, pois como já dito, ele nunca vai entrar no jogo diretamente, logo, estas características pouco importam. Primeiro, vamos configurar sua visão simples e faremos com que ele persiga o jogador. No Create Event, coloque:

{
   //Variáveis que serão alteradas no futuro
   var_atributo_velocidade = 0;
   var_atributo_dano = 0;
   var_atributo_visao = 0;
   var_atributo_vida = 0;

   //Variáveis que não serão alteradas
   var_alvo = objeto_jogador_geral
   var_provocado = 0;
   var_morto = false;
}

Como podemos ver, atribuímos muitas características ao nosso futuro oponente. Muitas estão zeradas pois no iremos alterar seus valores somente no objeto_inimigo_menor e objeto_inimigo_maior. Atribuímos também a variável var_alvo como o objeto_jogador_geral para ajudar a sermos menos redundantes nos códigos. Agora vamos trabalhar com elas agora.

No Step Event, coloque o seguinte:

{
//Vira o sprite do inimigo de acordo com a direção que ele anda...
image_angle = direction;

//Caso o jogador estteja dentro do raio de visão inimiga...
if distance_to_object(var_alvo) <= var_atributo_visao
    {
    
    //... e também se não houver nenhuma parede que atrapalhe a visão do inimigo...
    if !collision_line(x, y, var_alvo.x, var_alvo.y, obj_obstaculo_geral, 1, 1)
        {
        
        //A variável var_provocado será constantemente colocada com valor 90.
        //Como será uma variável para indicar tempo, 90 = 3 segundos (pois 30 = 1 segundo).
        var_provocado = 90;
        
        }
    
    }
    
//Se o inimigo estiver provocado...
if var_provocado > 0
    {
    
    //Diminui a variável provocado constantemente.
    //Como acima atribuímos o valor de 90, significa que o inimigo que perder 
    //a visão do jogador tentará encontrá-lo por mais 3 segundos, 
    //e depois irá desistir.
    var_provocado -= 1;
    
    //Tenta chegar ao jogador. O mp_potential_step é
    //muito poderoso pois é uma função que permite
    //as instâncias chegarem entre sí evitando sempre
    //obstáculos sólidos.
    mp_potential_step(var_alvo.x, var_alvo.y, var_atributo_velocidade, 0);
    
    }
}

Acabamos de utilizar as variáveis var_alvo, var_atributo_velocidade, var_atributo_visao, var_provocado. Vale á pena comentar um pouco sobre duas funções importantíssimas que acabamos de utilizar no código acima: collision_line e mp_potential_step.

A collision_line cria uma linha imaginária entre um ponto (x¹, y¹) e outro (x², y²), neste caso, entre o ponto do inimigo e o ponto da personagem. É extremamente útil, especialmente da forma que aplicamos acima: isso funciona como uma linha de visão do inimigo, onde ele só consegue "ver" o jogador caso não tenha nenhum objeto_obstaculo_geral entre os dois.

O mp_potential_step é com certeza a função dos deuses. Ela permite que objetos movam-se em direções á um ponto qualquer automaticamente desviando de obstáculos sólidos. Existe outra versão, chamada de mp_potential_step_object onde o objeto não evita obstáculos sólidos, mas sim apenas o objeto que foi especificado. Isso é útil, por exemplo, para fazer com que o inimigo verde passe somente pela parede verde, enquanto o inimigo amarelo passe somente pela parede amarela.

As próximas funções serão referentes á apanhar e morrer e, convenhamos, estas funções precisam de um tempero á mais, para que o jogador de fato saiba que acertou o inimigo e que o inimigo morreu. Para isso, criamos efeitos visuais.

3. Configurando efeitos visuais
Começamos fazendo o principal neste quesito: os sprites dos efeitos. Nesta ocasião, uma animação convincente é obrigatória, pois é intuito deste recurso. Criaremos dois sprites: sprite_efeito_sangue e sprite_efeito_morte (ambos em padrão 16x16 e origem centralizada).

Ofereço-lhe duas imagens que vão cair bem nesta ocasião, use-as se quiser:



Criemos então um novo objeto: objeto_efeito_geral. Diferente dos inimigos, não é necessário criar um objeto para cada efeito visual pois todos eles possuem o mesmo intuito, sempre: aparecem, fazem suas animações e depois somem. A única característica que devemos alterar é sua Depth: coloque em -1 (agora ele estará sempre acima visualmente de qualquer instância que tenha Depth igual ou maior que 0). Agora, no Create Event, defina uma velocidade menor que o padrão (1), algo como:

{
   image_speed = 0.25;
}

E no Other Event: Animation End, coloque:

{
   instance_destroy();
}

Nosso efeito visual está pronto para ser usado á qualquer instante agora.

4. Configurando o inimigo II
Agora podemos completar um pouco mais nosso objeto_inimigo_geral. Continue editando o Step Event (se quiser criar uma nova folha de código para ajudar a organizar a informação, fica á seu critério) com o seguinte:

{
//Se houver um ataque sobre o inimigo...
if place_meeting(x, y, objeto_ataque_geral)
    {
    
    //Se ele estiver vivo ainda...
    if var_morto == false
        {
        
        //Chamaremos o ataque que estiver sobre ele de "var_temporaria_ooo"...
        var_temporaria_ooo = instance_nearest(x, y, objeto_ataque_geral);
        
        //Reduziremos um ponto de sua vida...
        var_atributo_vida -= 1;
        
        //Destruiremos o ataque, pois este já fez sua função.
        with var_temporaria_ooo do
            instance_destroy();
            
        //E criaremos um efeito visual de sangue saindo do inimigo.
        var_temporaria_efeito = instance_create(x, y, objeto_efeito_geral);
        var_temporaria_efeito.sprite_index = sprite_efeito_sangue;
        
        }
    
    }
    
//Caso a vida dele esteja igual ou menor que zero...
if var_atributo_vida <= 0 && var_morto == false
    {
    
    //Então deve ser considerado morto.
    var_morto = true;
    
    //Cria-se um efeito visual temporário referente á isso.
    var_temporaria_efeito = instance_create(x, y, objeto_efeito_geral);
    var_temporaria_efeito.sprite_index = sprite_efeito_morte;
    
    //E destrói a instância, para tirá-la do jogo.
    instance_destroy();
    
    }
}

Se você analisar bem o código acima, compreenderá que agora o inimigo efetivamente apanha e morre depois que atacado um certo número de vezes. Ele está qualquer pronto, mais ainda não faz o principal: atacar o jogador. Podemos fazer isso de milhares de maneiras diferentes, mas eu pretendendo fazer isto da maneira mais simples o possível.

5. Interação entre inimigo e personagem
Para isso acontecer, precisamos fazer mais alterações na nossa personagem. A ideia vai ser a seguinte: fazer com que, ao tomar dano, o personagem fique inatingível por um curto período, podendo tomar outro dano somente quando esta invencibilidade acabar, e isso será muito simples de fazer.

Para isso, voltemos ao nosso objeto_jogador_geral. No Create Event, acrescente três novas variáveis:

{
var_inatingivel = 0;
var_atributo_vida = 10;
var_morto = false;
}

A var_inatingivel vai servir como um timming para definir o período que o jogador vai ficar totalmente inatingível e, portanto, incapaz de tomar outro ataque inimigo. Vamos trabalhar só com ela por enquanto para não confundirmos todas as informações. Ainda no Step event, acrescente o seguinte:

{
if var_inatingivel > 0
    var_inatingivel -= 1;
}

É interessante sempre comunicarmos ao jogador real do que está se passando no jogo, para que ele entenda realmente como as coisas funcionam. Neste caso, é importante deixarmos claro que o jogador está invencível por este período. A maneira mais clássica, usada pela maioria dos jogos de antigamente, para isso é fazermos o personagem ficar "piscando" durante sua invencibilidade. No Step event, acrescente então:

{
//Efeito visual de invencibilidade
if frac(var_inatingivel/2) == 0
    visible = true;
else
    visible = false;
}

Pronto! Agora nossa personagem sabe como apanhar, resta a nós ensinar aos inimigos como bater. Só que nós vamos fazer isso de uma maneira inesperada. Assim como a lógica do personagem e os obstáculos, onde ao invés de ensinarmos os obstáculos a interromperem a personagem, ensinamos a própria personagem a não atravessar os obstáculos, iremos ensinar a personagem á apanhar dos inimigos, e não os inimigos baterem na personagem. Confuso? Relaxe, que você irá entender.

Anteriormente, já tínhamos adicionados as variável "var_atributo_vida = 10" e "var_morto = false" no Create Event do objeto_jogador_geral. Neste caso, ainda no Step Event do objeto_jogador_geral, acrescente o seguinte:

{
    //Se a distância entre o inimigo e o jogador for de 1 pixel, ou seja, estarem extremamente pertos...
    if distance_to_object(objeto_inimigo_geral) <= 1
        {
        
        //E a personagem não esteja em período de invencibilidade...
        if var_inatingivel <= 0
            {
            
            //Chamaremos

            //A personagem perde pontos de vida e torna-se intocável por 3* segundos. 
            //* Lembre-se que 30 = 1 segundo, em termos de tempo.
            var_atributo_vida -= 1;
            var_inatingivel = 90;
            
            //E por fim criaremos um efeito visual de sangue saindo da personagem.
            var_temporaria_efeito = instance_create(x, y, objeto_efeito_geral);
            var_temporaria_efeito.sprite_index = sprite_efeito_sangue;
            
            }
        
        }
    
    //Se a vida da nossa personagem chegar á estaca zero...
    if var_atributo_vida <= 0 && var_morto == false
        {
        
        //Ela deve ser considerada morta.
        var_morto = true;
        var_inatingivel = 9999;
    }
}

Com este código, fechamos o nosso sistema de combate básico do jogo. Falta só alguns detalhes á serem feitos.

6. Configurando o inimigo III
Chegamos ao nosso último passo no que diz respeito aos nosso queridos inimigos. Agora, vamos atribuir características diferentes á cada um de nossos inimigos. No momento nós só temos dois: objeto_inimigo_menor e objeto_inimigo_maior, mas nada impede que você crie muitos outros (aliás, eu encorajo que você o faça depois!).

No Create Event do objeto_inimigo_menor, coloque o seguinte código:

{
   //Chama-se o evento do Parent, ou seja, copia as variáveis do seu superior, o objeto_inimigo_geral
   event_inherited();
   
   //Variáveis alteradas de acordo com o inimigo. Todas com valores baixos, de acordo com o jogo, pois é o primeiro inimigo e não deve representar tanta ameaça, especialmente para um jogador novato.
   var_atributo_velocidade = 0.5;
   var_atributo_dano = 1;
   var_atributo_visao = 64;
   var_atributo_vida = 3;
}

No Create Event do objeto_inimigo_maior, coloque o seguinte código:

{
   event_inherited();
   
   //Inimigo um pouco mais agressivo que o primeiro, com valores mais altos.
   var_atributo_velocidade = 0.75;
   var_atributo_dano = 2;
   var_atributo_visao = 80;
   var_atributo_vida = 5;
}

Vale á pena comentar novamente sobre a função "event_inherited();". que adicionamos acima. Se não tivéssemos colocado ela no início do código, certamente teríamos um erro ao executar o código, pois algumas variáveis importantes características dos inimigos (como a "var_alvo = objeto_jogador_geral;" e a "var_provocado = 0;") se encontram apenas no objeto-pai.

Por outro lado, se tivéssemos colocado um "event_inherited();" no final do código, também não teria dado certo, pois todas as alterações que fizéssemos seriam em vão, já que todos os valores voltariam á ser 0, igual o objeto-pai (o código é executando sempre da primeira em direção á última linha).

Entendido isso, podemos passar para o último passo desta aula.

7. Morte, Game Over e outros detalhes
Assim como tivemos que adicionar detalhes visuais para quando o inimigo morre, também teremos que o fazer para quando a personagem chega ao seu destino final. Obviamente, a morte da nossa personagem é muito mais relevante que a morte de um inimigo comum e, por este motivo, devemos fazer algo mais impactante.

A ideia é criarmos uma cena em que torne claro que o jogador falhou ao deixar sua personagem morrer e, portanto, terá que tentar novamente desde o começo da fase.

Para isso, podemos criar um novo objeto: o objeto_game_over. Com este nome intuitivo, podemos começar á trabalhar nele. Ele não precisa de sprites, nem masks e nem coisas do tipo, apesar disso, ele irá participar diretamente do jogo e não através de um representante. Porém, como sendo um recurso do jogo irrefutável, deve estar acima de qualquer coisa, logo, terá que ter uma Depth extremamente baixa. Algo em torno de -99 é o suficiente.

Agora, vamos dar o primeiro passo ao que chamamos de Interface do usuário, embora isso é um assunto que será abordado em uma próxima aula. Agora, só faremos o necessário.
No objeto_game_over, vamos trabalhar alguns conceitos diferentes: alarms e draw event. Antes de mais nada, vamos ao Create Event do objeto_game_over:

{
//Torna todas as instâncias da room invisíveis, inclusive ela
with all do
    visible = false;

//Torna-se visível novamente
visible = true;

//Dois alarms, [1] para ser adicionado nesta exato momento e o [0], em 1 segundo, pois 30 = 1 segundo real.
alarm[1] = 1;
alarm[0] = 30;
}

Colocamos dois alarms para executar suas ações com intuitos diferentes. Em geral, coloca-se alarms com número irrisórios, como o caso do alarm[1] = 1, para que ele seja executado exatamente depois do Crate Event e do Other: Room Start do objeto em questão. Já no caso do alarm[0] = 30, colocamos em um perído não tão grande assim (1 segundo apenas), porém já faz uma certa diferença para o jogador real, já que ele vai, de fato, demorar um pouco para ser ativado.

No evento do Alarm[1] do objeto_game_over, que será ativado praticamente no momento que o objeto_game_over entrar em cena, coloque:

{
//Isso vai fazer com que o jogo congele por 2000 milissegundos, ou 2 segundos).
sleep(2000);
}

Já no evento do alarm[0] coloque o seguinte:

{
//Isso fará com que a room reinicie-se, como forma de punição ao jogador por ter deixado a personagem morrer.
room_restart();
}

Agora, no Draw Event, vamos realmente ao que interessa: a mensagem do jogo ao jogador real de que ele falho e deve tentar novamente. O Draw Event nada mais é que desenho de vetores, formas geométricas e textos na tela do jogador. Não é algo que interaja com as instâncias da room, tornando assim o meio ideal para criar a interface do jogador.

Como vamos trabalhar com textos, é interessante primeiro criar uma fonte. A Font pode ser criada como qualquer outro recurso comum. Lá, você pode pegar os caracteres de uma fonte qualquer instalada no seu computador, mas deve levar em conta que isso não significa que o jogador que baixar o seu jogo terá a mesma fonte no computador dele, portanto, prefira fontes comuns e populares (isso explica do por quê você baixa aquele jogo em coreano e todas as "letras" transformam-se em quadrados iguais).
Você tem de considerar também que nem todas as fontes são adaptadas ao português e, portanto, não terão caracteres de letras acentuadas. Para verificar se a fonte possui tais caracteres, clique em "All" para que o character range fique de 0 á 255, e depois verifique dentro do jogo. A fonte pode ser á sua escolha, no tamanho que achar melhor, negrito ou não, itálico ou não. Apenas dê o nome á ela de fonte_padrao0.

Agora, para finalizar, no Draw Event do objeto_game_over, coloque o seguinte código:

{
    //Torna todas as formas geométricas, textos e vetores á seguir na cor PRETA.
    draw_set_color(c_black)
    
    //Desenha um retângulo com essas característica.
    draw_rectangle(x-96, y - 32, x + 96, y + 32, 0);
    
    //Torna todas as formas geométricas, textos e vetores á seguir na cor BRANCA.
    draw_set_color(c_white);
    
    //Torna todos os textos á seguir centralizados tanto horizontalmente quanto verticamente.
    draw_set_halign(fa_center);
    draw_set_valign(fa_middle);
    
    //Aplica a fonte que criamos anteriormente.
    draw_set_font(fonte_padrao0);
    
    //Desenha o texto conforme as características dadas acima.
    draw_text(x, y, "Uh oh!#Tente novamente!");
}

Agora, vamos á tão esperada...


8. Finalização
Verifique se tudo está OK durante o teste. Característica á se verificar:

- Inimigos perseguem o jogador.
- Inimigos atacam o jogador.
- Inimigos não adentro paredes.
- Jogador apanha somente em no mínimo de 3 em 3 segundos.
- Jogador morrer.
- Inimigos apanham e morrem.
- Cena de game over, e logo após a sala reinicializa.

Se tudo ocorrer como o esperado, parabéns! Mais um passo concluído. Vale á pena lembrar que o seu personagem consegue andar para trás, então use esta função durante o teste para atacar ao mesmo tempo que se afasta do inimigo!
Ufa! até a próxima!


3 comentários:

  1. Aee finalmente voltaram com os Tuto de GM

    Você está me ajudando muito!!! Citarei seu blog nos creditos nos games que estou criando!

    ResponderExcluir
    Respostas
    1. Obrigado amigo, é uma honra!

      Excluir
    2. Poxa vida, ajudou muuuuuuiiito , você não tem noção!
      Valeu cara, muito bom , parabéns!

      Excluir

Dúvidas? Pergunte!
Ajudou? Agradece!
:]