[Tutorial] Com lasers!!!

Ver o tópico anterior Ver o tópico seguinte Ir em baixo

Qualidade [Tutorial] Com lasers!!!

Mensagem por saim em Qua 21 Dez 2011, 14:27

Título: Lasers simples
Nível: Intermediário
Pré-requisitos: Domínio das funções lengthdir_x e lengthdir_y. Domínio dos loops (laços).
Descrição: 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.

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:
Conhece alguma forma de aproximação mais rápida (que chegue no resultado com menos iterações)? Não se acanhe, me mande uma MP ou poste diretamente nos comentários. Eu farei alguns comparativos e, se for verificado que seu método é mais eficiente (em todos os casos), eu altero o 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.


Última edição por saim em Qui 22 Dez 2011, 16:27, editado 3 vez(es)

saim

Ranking : Nota B
Número de Mensagens : 2964
Idade : 38
Data de inscrição : 14/01/2011
Notas recebidas : C-D-A-B
Reputação : 121
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 1
   : 0
   : 3

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por PedroX em Qua 21 Dez 2011, 23:52

Alternativas:
Para achar a vitima mais próxima, há outro método. Lista de prioridade.
Código:
vitimas = ds_priority_create();
with (all) {
 if (collision_line(other.x, other.y, destino_x, destino_y, id, 0, 1)) {
    ds_priority_add(other.vitimas, id, point_distance(x, y, other.x, other.y));
}
}
if (!ds_priority_empty(vitimas))
vitima = ds_priority_find_min(vitimas);
ds_priority_destroy(vitimas);

Agora podemos fazer o seguinte:
Código:
distance=point_distance(x, y, vitima.x, vitima.y);
direction=point_direction(x, y, vitima.x, vitima.y);
while(collision_line(x, y, x+lengthdir_x(distance, direction), y+lengthdir_y(distance, direction), vitima, 0, 1))
{
distance-=1;
if distance<1 break;
}

Achei o tutorial bem interessante.
Mais um bom tutorial para o fórum.
Parabéns.

Até mais!

PedroX

Ranking : Nota C
Número de Mensagens : 6034
Idade : 21
Data de inscrição : 26/07/2008
Notas recebidas : C+B
Reputação : 286
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   :
   :
   :

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por primz em Qui 22 Dez 2011, 08:20

muito bom tutorial!

para a linha não ficar tão "chocho" pode usar a função:

Código:

draw_line_width(x1, y1, xL, yL,3);
em vez de só:
Código:

draw_line(x1, y1, xL, yL);

para ela ficar mais larga!

eu usei 3 mas podem trocar o valor.

como ficou o meu:



Razz

primz

Ranking : Nota C
Número de Mensagens : 321
Idade : 20
Data de inscrição : 09/02/2010
Notas recebidas : C
Reputação : 2
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 0
   : 0
   : 1

http://www.primz.weebly.com

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por saim em Qui 22 Dez 2011, 09:22

@Pedrø: Excelente dica! Nunca tinha visto utilidade pras Priority Queues, e essa utilidade estava aí, dançando na minha frente! No caso da vítima estar perto do final do laser, acaba sendo mais lento, mas essa é uma situação, por definição, difícil de acontecer (já que o final do laser é localizado no ponto "muito longe"). Só não vou mudar a definição da direção do laser, porque a origem (âncora) da vítima dificilmente estará num ponto da linha do laser.
Não fiz nenhum teste de performance ainda, mas se alguém fosse colocar dinheiro nisso, eu apostaria na sua alternativa.

@primz: Obrigado pela dica, mas usar width é quase tão chocho quanto usar uma linha simples. Já vi um tutorial (em outra comunidade) que usava um sprite especialmente elaborado e image_xscale, que dava um efeito sensacional - o laser ficava todo irregular, vibrante, potente. Parecia realmente um raio de energia nociva.
Não mencionei isso porque foge do escopo do tutorial. Meu objetivo, aqui, é só atender aquela definição do início. Deixei a parte gráfica por conta da criatividade do desenvolvedor.

saim

Ranking : Nota B
Número de Mensagens : 2964
Idade : 38
Data de inscrição : 14/01/2011
Notas recebidas : C-D-A-B
Reputação : 121
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 1
   : 0
   : 3

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por Klior em Qui 22 Dez 2011, 12:23

Parabéns saim, realmente um ótimo tutorial, tudo muito bem explicado e detalhado.

Klior

Número de Mensagens : 426
Idade : 25
Data de inscrição : 07/03/2010
Reputação : 13
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 0
   : 0
   : 0

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por Stryker em Qui 22 Dez 2011, 12:37

Ja fiz um tutorial desse aki ja so que ta numa antiga conta ke foi excluida.
so ke vc feiz com codigos diferentes.

mais ta legal


Aki olha http://gmbr.forumeiros.com/t15799-tutorialtiro-em-top-down?highlight=tiro
sei ke fui banido na kela conta mais to tentando recomeçar


a e eu sei deixa lazer infinito

Stryker

Número de Mensagens : 9
Data de inscrição : 15/12/2011
Reputação : 1
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por saim em Qui 22 Dez 2011, 17:04

Olhei lá o tutorial, ele repete 9999 vezes uma linha, ou até acertar o alvo. Em nenhuma das duas situações é muito eficiente. Veja bem, supondo que o alvo esteja numa distância de 200 pixels, você vai repetir essa linha 200 vezes. Com essa aproximação progressiva, um alvo a 800 pixels (que é a diagonal de uma room padrão) precisa repetir o código apenas 10 vezes. Com o adendo do Pedrø (e uma idéiazinha minha, que usa a sprite do alvo), apenas 6 vezes.

Realmente seu código funciona, mas recomendo mudar o paradigma usado na aproximação.

saim

Ranking : Nota B
Número de Mensagens : 2964
Idade : 38
Data de inscrição : 14/01/2011
Notas recebidas : C-D-A-B
Reputação : 121
Insignia 1 x 0 Insignia 2 x 0 Insignia 3 x 0
Prêmios
   : 1
   : 0
   : 3

Voltar ao Topo Ir em baixo

Qualidade Re: [Tutorial] Com lasers!!!

Mensagem por Conteúdo patrocinado Hoje à(s) 03:56


Conteúdo patrocinado


Voltar ao Topo Ir em baixo

Ver o tópico anterior Ver o tópico seguinte Voltar ao Topo

- Tópicos similares

 
Permissão deste fórum:
Você não pode responder aos tópicos neste fórum