Trajetória com linha bezier entre 2 pontos
Autor: jhone wellington Carneiro | Publicado: 2013-03-23T08:10:00-07:00









Trajetória com linha bezier entre 2 pontos

Neste post será explicado como fazer um objeto seguir uma determinada
trajetória curvilínea entre dois pontos quaisquer. Isso é útil quando você
precisa fazer um objeto percorrer um trajeto e chegar em determinado ponto. É
um script de simples manuseio e configuração. Vamos lá.


&nbsp;


O script é bem simples, não é nada mais nada menos do que a transcrição da
função de uma curva bezier cúbica. Como é em função do tempo, é ideal para o
caso, pois pode-se usar o Time.time para o deslocamento do objeto.

Quando eu criei esse script, eu necessitava para disparar um projétil contra um
alvo e o tiro tinha que ser certeiro. Inicialmente a minha idéia foi usar a
física calculando a distância entre os dois objetos e calculando a velocidade
inicial necessária para o projétil atingir o alvo com um ângulo de 45 graus.
Mas me dei conta de que se o objeto não estivesse na mesma altura do outro, o
tiro não chegaria.
Após isso, pensei em utilizar a propriedade Vector3.Slerp() do Unity, mas
depois de alguns testes desisti e fui investigar outro método.
Lembrei que uma vez eu havia visto uma animação feita com linhas bezier e fui
procurar a respeito e ta-dam!
Com uma simples função é possível criar uma linha curvilínea!

 
 
  
  
  
  
  
  
  
  
  
  
  
  
 
 
 

 

Para saber mais detalhes, aconselho a leitura na wikipedia: 

Vamos lá então. Para criar uma curva bezier do tipo cúbica são necessários 4
pontos: o ponto inicial, o ponto final e dois pontos auxiliares que criarão a
curva. Neste exemplo que eu vou dar, os 4 pontos estão na cena organizados
dessa maneira:

 

O script BezierCurve está no objeto Bezier.
view sourceprint?




Código:


01.public class BezierCurve : MonoBehaviour
02.{
03. //points that will create the bezier curve
04. public Transform pos1;
05. public Transform pos2;
06. private GameObject pos1_aux;
07. private GameObject pos2_aux;
08. 
09. void Awake()
10. {
11. //pos1_aux and pos2_aux are the children of the main position
12. pos1_aux = (GameObject)pos1.GetChild(0).gameObject;
13. pos2_aux = (GameObject)pos2.GetChild(0).gameObject;
14. }



Os objetos pos1_aux e pos2_aux serão encontrados pelo script e movimentando
eles, será possível criar a curva.
Como já temos a posição dos 4 pontos, basta agora passar esses 4 pontos em
função do tempo.
view sourceprint?


Código:


01.//take the point and send to DoBezier4Points to
calculate
02.public Vector3 GetPointAtTime(float t)
03.{
04. //create a loop - this garantee that t will never be &gt; 1
05. t = t % 1.0f;
06. if (t &lt; 0)
07. {
08. t = 1.0f + t;
09. }
10. 
11. //create the vectors just to make a clean code
12. Vector3 p0 = pos1.position; //initial point
13. Vector3 p1 = pos1_aux.transform.position; //auxiliar of initial point
14. Vector3 p2 = pos2_aux.transform.position; //auxiliar of end point
15. Vector3 p3 = pos2.position; //end point
16. 
17. //call the function DoBezier4Points to calculate
18. return DoBezier4Points(t, p0, p1, p2, p3);
19.}
20. 
21. //calculate the position of the point at time t
22. public Vector3 DoBezier4Points(float t, Vector3 p0, Vector3 p1, Vector3 p2,
Vector3 p3)
23. {
24. //this is the function to create a cubic bezier curve
25. Vector3 point = Vector3.zero;
26. float OneMinusT = (1f - t);
27. 
28. point += (Mathf.Pow(OneMinusT, 3) * p0);
29. point += (3 * Mathf.Pow(OneMinusT, 2) * t * p1);
30. point += (3 * OneMinusT * Mathf.Pow(t, 2) * p2);
31. point += (Mathf.Pow(t, 3) * p3);
32. 
33. return point;
34. }



Agora basta enxergar a curva criada. Para isso, usa-se a função OnDrawGizmos().
view sourceprint?


Código:


01.//Draw the gizmo
02.public void OnDrawGizmos()
03.{
04. pos1_aux = (GameObject)pos1.GetChild(0).gameObject;
05. pos2_aux = (GameObject)pos2.GetChild(0).gameObject;
06. 
07. for (float t = 0.0f; t &lt; 1.0f; t += 0.1f) //change this 0.1f to see more
points in the screen
08. {
09. Gizmos.DrawIcon(this.GetPointAtTime(t), "BezierCurve.png");
10. }
11.}



Reparem nessa linha de código:
view sourceprint?
1.Gizmos.DrawIcon(this.GetPointAtTime(t),
"BezierCurve.png");

Esse “BezierCurve.png” não é nada mais nada menos do que uma imagem que eu
criei 10x10pixels de cor amarela para poder visualizar os pontos no editor. Se
você não fizer isso, vai aparecer um quadrado branco. (ps: deve-se colocar a
imagem dentro de uma pasta chamada Gizmos).

Com a curva criada, basta agora fazer com que um objeto siga a trajetória. Para
isso, criamos uma interface que chamará a função GetPointAtTime(float t) e
adicionaremos essa interface a classe BezierCurve
view sourceprint?


Código:


01.public interface IBezier
02.{
03. Vector3 GetPointAtTime(float t);
04.}
05. 
06.public class BezierCurve : MonoBehaviour, IBezier
07.{
08....
09.}



Crie um objeto qualquer que seja visível e adicione um script nele que irá
move-lo de acordo com os pontos da trajetória.
view sourceprint?


Código:


01.public class FollowPath : MonoBehaviour
02.{
03. IBezier Ipath;
04. //the path of the object
05. public GameObject path;
06. 
07. //the speed of the object
08. public float speed;
09. 
10. void Start()
11. {
12. //find the path
13. Ipath = path.GetComponent(typeof(IBezier)) as IBezier;
14. }
15. 
16. void Update()
17. {
18. //make the object follow the path
19. transform.position = Ipath.GetPointAtTime(Time.time * speed);
20. }
21.}



Os dois scripts finais ficam assim:

BezierCurve.cs
view sourceprint?


Código:


01.using UnityEngine;
02.using System.Collections;
03.using System.Collections.Generic;
04.using System;
05. 
06.public interface IBezier
07.{
08. Vector3 GetPointAtTime(float t);
09.}
10. 
11.public class BezierCurve : MonoBehaviour, IBezier
12.{
13. //points that will create the bezier curve
14. public Transform pos1;
15. public Transform pos2;
16. private GameObject pos1_aux;
17. private GameObject pos2_aux;
18. 
19. void Awake()
20. {
21. //pos1_aux and pos2_aux are the children of the main position
22. pos1_aux = (GameObject)pos1.GetChild(0).gameObject;
23. pos2_aux = (GameObject)pos2.GetChild(0).gameObject;
24. }
25. 
26. //take the point and send to DoBezier4Points to calculate
27. public Vector3 GetPointAtTime(float t)
28. {
29. //create a loop
30. t = t % 1.0f;
31. if (t &lt; 0)
32. {
33. t = 1.0f + t;
34. }
35. 
36. Vector3 p0 = pos1.position;
37. Vector3 p1 = pos1_aux.transform.position;
38. Vector3 p2 = pos2_aux.transform.position;
39. Vector3 p3 = pos2.position;
40. 
41. return DoBezier4Points(t, p0, p1, p2, p3);
42. }
43. 
44. //calculate the position of the point at time t
45. public Vector3 DoBezier4Points(float t, Vector3 p0, Vector3 p1, Vector3 p2,
Vector3 p3)
46. {
47. Vector3 point = Vector3.zero;
48. float OneMinusT = (1f - t);
49. 
50. point += (Mathf.Pow(OneMinusT, 3) * p0);
51. point += (3 * Mathf.Pow(OneMinusT, 2) * t * p1);
52. point += (3 * OneMinusT * Mathf.Pow(t, 2) * p2);
53. point += (Mathf.Pow(t, 3) * p3);
54. 
55. return point;
56. }
57. 
58. //Draw the gizmo
59. public void OnDrawGizmos()
60. {
61. //this is necessary to work in the editor mode
62. pos1_aux = (GameObject)pos1.GetChild(0).gameObject;
63. pos2_aux = (GameObject)pos2.GetChild(0).gameObject;
64. 
65. //Show the path in the screen. Changing the 0.1f will increase/decrease the
points in the screen
66. for (float t = 0.0f; t &lt; 1.0f; t += 0.1f)
67. {
68. Gizmos.DrawIcon(this.GetPointAtTime(t), "BezierCurve.png");
69. }
70. }
71.}




FollowPath.cs
view sourceprint?


Código:


01.using UnityEngine;
02.using System.Collections;
03. 
04.public class FollowPath : MonoBehaviour
05.{
06. IBezier Ipath;
07. //the path of the object
08. public GameObject path;
09. 
10. //the speed of the object
11. public float speed;
12. 
13. void Start()
14. {
15. //find the path
16. Ipath = path.GetComponent(typeof(IBezier)) as IBezier;
17. }
18. 
19. void Update()
20. {
21. //make the object follow the path
22. transform.position = Ipath.GetPointAtTime(Time.time * speed);
23. }
24.}




Resultado da primeira parte

 


SEGUNDA PARTE

Trajetória com linha bezier entre 2 pontos – parte 2

Como dito no post anterior, vou descrever como utilizar o estudo realizado com
a linha bezier em um jogo.
Imagine: depois de várias fases, finalmente o jogador chega a última e está
prestes a enfrentar o chefão. Para derrotá-lo, é necessário coletar itens e
somente quando tiver o suficiente (vamos dizer 3), ele poderá atirar contra o
inimigo.
Ao final do post, adicionei um exemplo similar a esse.

Como já explicado, a linha bezier funciona através de 4 pontos: a origem e o
destino mais 2 pontos auxiliares que criam a curva. No jogo, não precisamos ver
essa curva, só precisamos garantir que o ponto inicial e final estejam
posicionados corretamente. Há várias maneiras de se fazer isso. No exemplo
utilizado é bem simples, já que eu possuo somente um inimigo. Caso no jogo
tenha vários inimigos, basta adicionar o script no seu personagem principal,
colocar o ponto inicial como o próprio objeto e o ponto final como o inimigo e
quando instanciar o projétil, atualizar a posição de acordo com a curva.

Neste exemplo, eu adicionei o script da linha bezier diretamente no projétil,
portanto, a posição inicial é o jogador e a posição final é o canhão.


Código:


1.posInicial =
GameObject.FindGameObjectWithTag("Player").transform.position;
2.posFinal = GameObject.FindGameObjectWithTag("Cannon").transform.position;



Os outros 2 pontos auxiliares são definidos dentro da função GetPointAtTime.
Simplesmente coloco eles 5 unidades acima do ponto de origem e isso faz com que
se tenha um efeito bom.


Código:


1.Vector3 p0 =
pos1; //origem
2.Vector3 p1 = pos1 + new Vector3(0f,5f,0f); //auxiliar 1
3.Vector3 p2 = pos2 + new Vector3(0f, 5f, 0f); //auxiliar 2
4.Vector3 p3 = pos2; //destino



É necessário criar uma condição para que, quando a bala atingir o canhão, este
e a bala explodam.


Código:


01.void OnTriggerEnter(Collider other)
02.{
03.if (other.gameObject.tag == "Cannon")
04.{
05.Instantiate(explosion, transform.position, transform.rotation);
06.Destroy(posFinal.gameObject);
07.Destroy(gameObject);
08.}
09.}



E agora é só fazer com a que o projétil se desloque. Como a função da linha
bezier é em função do tempo, utiliza-se uma variável que adiciona frame a frame
o tempo do último frame. Assim é possível controlar a velocidade.


Código:


1.void Update()
2.{
3.temp += Time.deltaTime; //a cada frame, adiciona-se a essa variável que é
utilizada pela função posteriormente
4.this.transform.position = GetPointAtTime(temp, posInicial,
posFinal.position);
5.}




O código final fica assim:

BezierCurve.cs


Código:


01.using UnityEngine;
02.using System.Collections;
03.using System.Collections.Generic;
04.using System;
05. 
06.public class BezierCurve : MonoBehaviour
07.{
08.//posição inicial e final
09.private Vector3 posInicial = new Vector3();
10.private Transform posFinal;
11. 
12.//prefab que contém a explosão
13.public GameObject explosion;
14. 
15.//tempo auxiliar
para deslocar o projétil
16.private float temp = 0f;
17. 
18.void Start()
19.{
20.posInicial =
GameObject.FindGameObjectWithTag("Player").transform.position;
21.posFinal = GameObject.FindGameObjectWithTag("Cannon").transform;
22.}
23. 
24.void Update()
25.{
26.temp += Time.deltaTime; //a cada frame, adiciona-se a essa variável que é
utilizada pela função posteriormente
27.this.transform.position = GetPointAtTime(temp, posInicial,
posFinal.position);
28.}
29. 
30.void OnTriggerEnter(Collider other)
31.{
32.if (other.gameObject.tag == "Cannon")
33.{
34.Instantiate(explosion, transform.position, transform.rotation);
35.Destroy(posFinal.gameObject);
36.Destroy(gameObject);
37.}
38.}
39. 
40.public Vector3 GetPointAtTime(float t, Vector3 pos1, Vector3 pos2)
41.{
42.//create a loop
43.t = t % 1.0f;
44.if (t &lt; 0)
45.{
46.t = 1.0f + t;
47.}
48. 
49.Vector3 p0 = pos1;
50.Vector3 p1 = pos1 + new Vector3(0f,5f,0f);
51.Vector3 p2 = pos2 + new Vector3(0f, 5f, 0f);
52.Vector3 p3 = pos2;
53. 
54.return DoBezier4Points(t, p0, p1, p2, p3);
55.}
56. 
57.public Vector3 DoBezier4Points(float t, Vector3 p0, Vector3 p1, Vector3 p2,
Vector3 p3)
58.{
59.Vector3 point = Vector3.zero;
60.float OneMinusT = (1f - t);
61. 
62.point += (Mathf.Pow(OneMinusT, 3) * p0);
63.point += (3 * Mathf.Pow(OneMinusT, 2) * t * p1);
64.point += (3 * OneMinusT * Mathf.Pow(t, 2) * p2);
65.point += (Mathf.Pow(t, 3) * p3);
66. 
67.return point;
68.}
69.}






&nbsp;


&nbsp;


&nbsp;


&nbsp;


Dentro/fora da pista

detectar se um objeto está dentrou ou fora do trajeto delimitado.

Existem várias maneiras de se fazer isso e entre essas as que eu pensei foram:
colocar vários triggers ao longo da pista e verificar se o objeto está dentro
deles (solução fácil e rápida, porém nada otimizado), triggers ao redor da
pista e quando o objeto tocasse e destocasse consideraria fora… (vou parar por
aqui porque já deu para perceber que isso não funcionaria de maneira alguma) e
outra é que você verificaria por imagem (uma imagem preto e branco no fundo e
se o objeto está em cima do branco, está dentro, se está em cima do preto, está
fora).
Claro que, falando cruamente assim, parece necessitar de muito processamento já
que teria que pegar pixel a pixel, frame a frame e verificar com o objeto. Mas
é possível otimizar isso de uma maneira simples.
O unity tem uma função chamada GetPixels() que pega todos os pixels da imagem e
armaneza em um array com as respectivas cores.


Código:


1.//Cria um array
do tipo Color do tamanho da textura
2.Color[] arrayTrack = new Color[texture_width * texture_heigth];
3.//pega a textura do objeto
4.Texture2D temp = (Texture2D)gameObject.renderer.material.mainTexture;
5.//aplica-se a função
6.arrayTrack = temp.GetPixels();




Fazendo isso, você já não precisa mais ler pixel a pixel e sim a posição de um
elemento em um array.
Assim aparece outro problema: a posição de um objeto é feita através de X e Y
(em um plano 2D. Não é necessário ler a posição Z do objeto) e o array
armazenado não é multidimensional, então ele vai de 0 a um número X.
Solução: transferir os elementos do array para um array multi-dimensional.


Código:


01.//cria-se um
array multi-dimensional com o tamanho da textura
02.Color[,] arrayMultTrack = new Color[temp.width, temp.heigth];
03.//Chama uma função que copiará os dados para o array multi-dimensional
04.ConvertMatrix(temp.width, temp.heigth);
05. 
06.//função que transfere dados de array para array multi-dimensional
07.void ConvertMatrix(int tamX, int tamY)
08.{
09.int currentValue = 0;
10. for(int i=0; i&lt;tamX; i++)
11. {
12. for(int j=0; j&lt;tamY; j++)
13. {
14. arrayMultTrack[i,j] = arrayTrack[currentValue];
15. currentValue++;
16. }
17. }
18.}



Como já tem o array multi-dimensional, não precisa mais do original.


Código:


1.arrayTrack =
null;



O próximo passo agora é fazer com que a posição do objeto seja relativa a
posição do array. Para isso, usa-se proporção entre o tamanho da textura e o
plano em que ela está.


Código:


1.//proporção entre
a textura e o plano
2.float propX = temp.width/transform.lossyScale.x;
3.float propY = temp.height/transform.lossyScale.y;
4. 
5.//faz com que o canto superior esquerdo do plano seja a origem - usa-se para
comparar a posição do objeto com o plano
6.Vector2 posZero = new Vector2(transform.position.x-(transform.lossyScale.x/2),
transform.position.y-(lossyScale.y/2));



Após isso, compara-se a posição entre o plano e o objeto e com isso se tem a
posição relativa do objeto em relação a textura.


Código:


01.//a variável
precisa ser do tipo inteiro pois irá usar este valor para pegar a cor no array
02.int posObjX = Mathf.FloorToInt((objeto.transform.position.x -
posZero.x)*propX);
03.int posObjY = Mathf.FloorToInt((objeto.transform.position.y -
posZero.y)*propY);
04. 
05.//compara-se agora a posição do objecto com o array
06.if(arrayMultTrack[posObjX, posObjY]==Color.white)
07.{
08. Debug.log("Dentro");
09.}
10.else
11.{
12. Debug.log("Fora");
13.}



Resumindo: cria-se uma textura preta e branca onde o branco é o traçado
desejado. Coloca-se essa textura em plano e o script nesse plano deverá ler os
pixels da textura e colocá-los em um array. Transfere os dados do array para um
outro que seja multi-dimensional. Cria-se uma origem na borda do plano e
proporção entre a textura e a posição do plano. Com esses dois valores é
possível verificar a posição do objeto em relação ao plano.

O código fonte dos dois scripts prontos estão aqui. Os comentários estão em
inglês.

ReadTrack.cs – coloca-se este no plano com a textura


Código:


01.using UnityEngine;
02.using System.Collections;
03. 
04.public class ReadTrack : MonoBehaviour
05.{
06. private Color[] arrayTrack;
07. public static Color[,] arrayMultTrack;
08. public static Vector2 posZero = new Vector2();
09. public static Vector2 proportion = new Vector2();
10. 
11. void Awake()
12. {
13. //create an array with the size of the track
14. arrayTrack = new Color[gameObject.renderer.material.mainTexture.width *
gameObject.renderer.material.mainTexture.height];
15. //get all the pixels from the track and add them to an array
16. Texture2D temp = (Texture2D)gameObject.renderer.material.mainTexture;
17. arrayTrack = temp.GetPixels();
18. 
19. //transfer the colors from the array to the Multidimensional Array
20. arrayMultTrack = new Color[temp.width, temp.height];
21. ConvertMatrix(temp.width,temp.height);
22. 
23. //do this to take the real scale of the floor
24. transform.parent = null;
25. 
26. //Get the position (0,0) of the plane relative to the world
27. posZero = new Vector2(transform.position.x-(transform.lossyScale.x/2),
transform.position.z+(transform.lossyScale.z/2));
28. 
29. //proportions between the texture and the plane
30. proportion.x = temp.width/transform.lossyScale.x;
31. proportion.y = temp.height/transform.lossyScale.z;
32. 
33. //delete the array
34. arrayTrack = null;
35. }
36. 
37. //convert an array to a multimensionalarray
38. void ConvertMatrix(int tamX, int tamY)
39. {
40. int currentValue = 0;
41. for(int i=0; i&lt;tamX; i++)
42. {
43. for(int j=0; j&lt;tamY; j++)
44. {
45. arrayMultTrack[i,j] = arrayTrack[currentValue];
46. currentValue++;
47. }
48. }
49. }
50. 
51. void OnApplicationQuit()
52. {
53. //delete the multidimensional array
54. arrayMultTrack = null;
55. }
56.}



PositionTrack.cs – coloca-se este com o objeto


Código:


01.using UnityEngine;
02.using System.Collections;
03. 
04.public class PositionTrack : MonoBehaviour
05.{
06. private Vector2 proportion = new Vector2(); //proportion between the plane
and the texture of the track
07. private Vector2 posTrack = new Vector2(); //position of the track relative
to the world
08. private Color[,] track; //vector containing the track
09. 
10. void Start()
11. {
12. //read all the informations from ReadTrack
13. proportion.x = ReadTrack.proportion.x;
14. proportion.y = ReadTrack.proportion.y;
15. posTrack.x = ReadTrack.posZero.x;
16. posTrack.y = ReadTrack.posZero.y;
17. track = ReadTrack.arrayMultTrack;
18. }
19. 
20. void Update()
21. {
22. 
23. //(position of the Jetski - position of the track) * proportion between
texture and plane
24. int posX =
Mathf.FloorToInt(Mathf.Abs((transform.position.x-posTrack.x)*proportion.x));
25. int posY =
Mathf.FloorToInt(Mathf.Abs((transform.position.z-posTrack.y)*proportion.y));
26.// print(posX+","+posY+","+track[posX,posY]); //caso
queira verificar a posição do objeto com o array
27. 
28. //verify if the jetski is inside or outside the track
29. if(track[posX,posY]==Color.white)
30. {
31. Debug.Log("Inside");
32. }
33. else
34. {
35. Debug.Log("Outside");
36. }
37. }
38.}






&nbsp;