sábado, 14 de janeiro de 2012

[Saim] Lasers simples

Qualidade
Nome: Lasers simples
Blogueiro: Saim

Descrissao: Esse tutorial mostra uma maneira de lidar com lasers. Não é parte do escopo desse tutorial ensinar a desenhar o laser, embora seja mostrada uma forma de se fazer isso.
Versao: Lite e Pro
Foto(s): [NAO TEM]


Tutorial:

Introdução

Você quer fazer um raio laser. Primeiro, vou deixar aqui a minha definição de raio laser em games.
Raio laser é uma linha que parte de um ponto e vai até outro ponto e é interrompida quando colide com um objeto, no ponto em que ocorre essa colisão, podendo ou não alterar alguma variável do objeto colidido
Saim, sua definição é diferente da minha! Eu quero que meu laser vá até o infinito!
Azar o seu. Eu só sei fazer de um ponto a outro. Normalmente é possível substituir "até o infinito" por "muito longe". Espero que sirva.

Uma vez que se defina esses dois pontos, verifica-se se existe algum objeto entre eles. Se houver, defina o objeto como "o objeto que o laser acertou" e defina o ponto final como o ponto imediatamente antes da colisão ocorrer. Repita esse procedimento até que não exista nenhum objeto entre o ponto inicial e o (novo) ponto final. Faça o que tiver que fazer com o objeto colidido e desenhe o laser de um ponto a outro.

Fim

Como fazer isso

Ah, você já sabia que o tutorial não acabava ali? Droga, eu queria fazer surpresa...
Bom, tem duas perguntas principais, aqui.

Como saber se há um objeto colidindo com a linha?
Como saber o ponto imediatamente anterior à colisão?


Pra primeira pergunta, existe uma função específica no game maker. É a collision_line. Ela te retorna a id da instância que colide com a linha definida pelos pontos (que o usuário define nos argumentos). Não havendo nenhuma instância, ela retorna o objeto especial noone. Deixa eu te apresentar os argumentos dela:
collision_line(x1, y1, x2, y2, obj, prec, notme)
x1,y1,x2,y2: Os valores de x e y dos pontos que definem a linha
obj: O objeto a ser verificada a colisão. Pode ser um objeto, um parent ou uma id.
prec: Se a colisão é precisa (true) ou não (false).
notme: Se o objeto que chama a função deve ser ignorado (true) ou levado em conta (false)
Assim, jogamos os pontos do laser nos argumentos da função e pronto, não apenas sabemos se alguém colide com a linha como também sabemos quem é.

A segunda pergunta só faz sentido se houver alguma colisão. Aí, a coisa complica um pouco. Se o laser for prefeitamente ortogonal, podemos simplificar usando os valores de bbox_(left, right, top, bottom) da instância colidida. Mas e se não for? Só temos a id da instância colidida e, por consequencia, sua posição (que, conforme veremos, não será utilizada). O que faremos é ir testando novos comprimentos de laser até chegar naquele que NÃO colide com nada, mas que se aumentar um pouquinho só, já colide. Isso significa ir tentando uma aproximação, ir reduzindo uma variação até um valor de precisão pré-definido. Quando a variação for menor que a precisão, saberemos que encontramos o ponto.
Podemos definir essa variação como um pixel, por exemplo, ou um valor que o jogador não vai notar que o ponto não está exato.

Existem diversas formas de se aproximar, mas a que me parece mais rápida é também a mais simples: soma-se ou subtrai-se (conforme a colisão aconteça ou não) um valor variável, que começa sendo a metade do comprimento do laser e vai sendo dividido por 2. Com poucas iterações reduz-se esse valor a menos que um. Obviamente, quanto maior o valor inicial, mais iterações serão necessárias, o que pesa no processamento.
Melhore esse tutorial:

Assim, estabelece-se a rotina:
0- Verifica-se se é necessária alguma iteração. Se não, desenha-se o laser e fim. Se sim, continua-se com a rotina.
1- Divide-se o comprimento do laser pela metade e define-se o valor inicial da variável a ser somada/subtraída ao comprimento do laser como o novo comprimento total do laser.
2- Verifica-se se há colisão do laser com o objeto a ser colidido (já que o comprimento do laser mudou). Divide-se o valor da variável por 2.
3- Se houve colisão, subtrai-se do comprimento total do laser, o valor da variável. Se não, soma-se esse valor.
4- Verifica-se se o valor da variável ficou menor que a precisão requerida.
5- Se sim, podemos parar com o procedimento, já chegamos à precisão requerida. Se não, voltamos ao passo 2.
Note que seria possível já entrar direto na rotina, ignorando o passo 0, mas acho mais eficiente entrarmos nela apenas nos casos em que ela for necessária. Afinal, mesmo que se chegue no resultado com poucas iterações, podemos ter muitos lasers operando ao mesmo tempo.
Note, também, que havendo mais de uma instância colidida, essa rotina encontrará aquela mais próxima do início do laser.
No passo 2, podemos também armazenar a id colidida em uma outra variável pra alterarmos alguma variável dela, como a vida, por exemplo.

Fim (mesmo) da teoria.

Script

O que eu vou apresentar a seguir é um script que não retorna nada, mas define o valor de algumas variáveis. Ele não retorna nada porque eu precisaria de 3 retornos: o valor em x do fim do laser, o valor em y do fim do laser e a id da instância colidida.
Então, se você quiser usar outros valores de variáveis, fique à vontade pra alterar o script.
Código:
/* uso do script:
** laser(x1, y1, x2, y2, obj, precisão)
** o script verifica se há colisão do laser com uma instancia do objeto "obj".
** havendo, ele aproxima o laser até o ponto de colisão com o objeto
** a id colidida é armazenada na variável "vitima"
** o ponto de colisão é armazenado nas variáveis "xL" e "yL"
*/
var x1, y1, alvo;
x1 = argument0; y1 = argument1; //posição inicial do laser
xL = argument2; yL = argument3; //posição final do laser
alvo = argument4;              //quem o laser vai procurar

//primeiro, verifica-se se o script é mesmo necessário
vitima = collision_line(x1, y1, xL, yL, alvo, 1, 1);
if (vitima == noone){ //se não há colisão
   exit;            //acaba com o script aqui mesmo
   }
//Se ainda estamos aqui, é porque HOUVE a colisão. Precisaremos de mais algumas variáveis.
var prec, comp, ang, soma, novaVitima;
prec = argument5;
comp = point_distance(x1, y1, xL, yL) / 2; //tamanho do laser, já pela metade pra acelerar o processo
ang = point_direction(x1, y1, xL, yL); //ângulo do laser
soma = comp; //valor a ser somado/subtraído no tamanho do laser, até achar o ponto

while (soma >= prec){
   xL = x1 + lengthdir_x(comp, ang); yL = y1 + lengthdir_y(comp, ang);
   novaVitima = collision_line(x1, y1, xL, yL, alvo, 1, 1);
   soma *= 0.5; //diminui o tamanho a ser somado/subtraído
   if (novaVitima == noone){ //se o comprimento atual é menor do que o que dá colisão
      comp += soma;        //aumenta o comprimento
      }
      else {                //se o comprimento atual é maior ou igual ao que dá colisão
         comp -= soma;    //diminui o comprimento
         vitima = novaVitima; //atualiza a instância mais próxima (podendo repetir o valor)
         }
   }


Não acabou ainda???

Bom, isso nos dá o ponto final pra desenharmos o laser e a instância que o laser colide. O que mais precisamos? Precisamos achar uma utilidade pra isso!
A seguir, alguns exemplos.

Desenhar o laser como uma linha
Coloque, no draw event:
Código:
draw_line(x1, y1, xL, yL);
Pode ser substituído por draw_line_color. Recomendo usar vermelho. Fica meio chocho, mas dá pro gasto.

Acertar o objPlayer, diminuir sua vida e permitir que ele se esconda atrás do objParede
Coloque, no step event
Código:
//sendo que x2, y2 é o ponto máximo do laser:
xL = x2; yL = y2;
laser(x1, y1, xL, yL, objParede, 1);
//agora, (xL, yL) é o ponto de colisão com a parede (se houver colisão) ou o ponto máximo, se não houver colisão
laser(x1, y1, xL, yL, objPlayer, 1);
//agora, (xL, yL) é o ponto de colisão com o player (se houver colisão) e não temos certeza do valor de "vitima"
if (vitima != noone){ //se há uma vítima
   if (vitima. object_index == objPlayer){ //se a vítima for o player (ou algum personagem que se machuque)
      vitima. vida -= 1; //faz algo com a vítima
      }
   }
Esse raciocínio serve pra inimigos também, obviamente. Se você usar um parent, pode facilitar alguma coisa.
Uma outra forma de verificar isso seria verificar a colisão diretamente no player e, depois, usar só um collision_line pras paredes, mas aí, você não teria o ponto de colisão na parede. Além do mais, espera-se que seja mais comum o laser acertar paredes do que acertar o player.

Uma bala rápida como uma bala
Jogos de tiro não são muito realistas porque normalmente é possível var a bala, sendo que na vida real, só com uma câmera muito rápida. Se você usar o raciocínio acima no evento de atirar (ao invés de usar o step) e NÃO desenhar o laser, o efeito será o de uma bala atingindo o alvo instantaneamente. O resto do realismo fica por sua conta. Você ainda pode usar o ponto (xL, yL) pra criar um efeito de tijolos sendo estilhaçados ou sangue, pra mostrar pro jogador o ponto que ele atingiu.

laser articulado
Você ainda pode fazer o laser rodar, ficar num canhão que se move, oscilar entra "atirando" e "sem atirar", etc, usando sua criatividade. Mude o valor das coordenadas do ponto final pra fazer o laser mudar de direção. Faça o ponto inicial depender da posição do objeto e você pode prender o laser num canhão que se move. Rode o script apenas quando uma variável for verdadeira e prenda essa variável num alarm e - voilà, você tem um laser intermitente. Use tudo isso num monte de objetos e ao invés de tirar a vida do player, acione um som ao colidir o laser e - pimba! - um jogo de espionagem.

Brinque bastante e, se descobrir novas utilidades para os lasers, comente aqui!

Abraços,

saim

Update!!! (22/12/2011)
Com a dica do Pedrø (logo abaixo), é possível reduzir drasticamente o comprimento inicial do laser e ainda mais drasticamente o tamanho da veriável que será reduzida até o valor da precisão, o que reduz drasticamente o numero de iterações e, consequentemente, aumenta a eficiência do script. Alterei uma coisinha aqui e outra ali, o script ficou com essa cara:
Código:
/* uso do script:
** laser(x1, y1, x2, y2, obj, precisão)
** o script verifica se há colisão do laser com uma instancia do objeto "obj".
** havendo, ele aproxima o laser até o ponto de colisão com o objeto
** a id colidida é armazenada na variável "vitima"
** o ponto de colisão é armazenado nas variáveis "xL" e "yL"
*/
var x1, y1, alvo;
x1 = argument0; y1 = argument1; //posição inicial do laser
xL = argument2; yL = argument3; //posição final do laser
alvo = argument4;              //quem o laser vai procurar (pode ser um objeto, um parent ou uma id)

//primeiro, verifica-se se o script é mesmo necessário
vitima = collision_line(x1, y1, xL, yL, alvo, 1, 1);
if (vitima == noone){ //se não há colisão
   exit;            //acaba com o script aqui mesmo
   }
// Se ainda estamos aqui, é porque HOUVE a colisão. Precisaremos de mais algumas variáveis.
// Mas antes, veremos qual é a vítima mais próxima e usaremos a distância até ela
// pra reduzir o número de iterações.
var vitPrio, prec, comp, ang, soma, novaVitima;
vitPrio = ds_priority_create(); //cria uma lista de prioridades
with(alvo){ //para todas as instâncias de "alvo"
   if (collision_line(x1, y1, other . xL, other . yL, id, 1, 0)) { //se está no caminho do laser
      //entra na lista, com a prioridade sendo a distância até o ponto de origem do laser
      ds_priority_add(vitPrio, id, point_distance(x, y, x1, y1));
      }
   }
vitima = ds_priority_find_min(vitPrio); //vitima passa a ser a instância mais próxima do laser

ds_priority_destroy(vitPrio); //me livro da lista, liberando memória
 
prec = argument5;
comp = point_distance(x1, y1, vitima . x, vitima . y); //tamanho do laser
ang = point_direction(x1, y1, xL, yL); //ângulo do laser
soma = point_distance(0, 0, vitima . sprite_width, vitima . sprite_height); //valor a ser somado/subtraído no tamanho do laser, até achar o ponto

while (soma >= prec){
   xL = x1 + lengthdir_x(comp, ang); yL = y1 + lengthdir_y(comp, ang);
   novaVitima = collision_line(x1, y1, xL, yL, alvo, 1, 1);
   soma *= 0.5; //diminui o tamanho a ser somado/subtraído
   if (novaVitima == noone){ //se o comprimento atual é menor do que o que dá colisão
      comp += soma;        //aumenta o comprimento
      }
      else {                //se o comprimento atual é maior ou igual ao que dá colisão
         comp -= soma;    //diminui o comprimento
         vitima = novaVitima; //só é util pra alguns casos muito específicos
         }
   }
Fiz uns testes de performance e, com esse novo script, coloco 300 canhões na room e consigo uma velocidade de 56 fps, ao passo que com o script anterior, consigo apenas 52.Creditos: Saim

Nenhum comentário:

Postar um comentário