Página 1 de 1

Rotar puntos en un plano 2D

NotaPublicado: 2008-06-22 19:27 @852
por creating021
Estoy haciendo ( al menos intento :lol: ) en un clon de asteroids ( de Atari ) usando Prima, no parece complicado y realmente no lo es... tan solo me he atascado en algo.

En el juego los asteroides y la nave rotan, ese movimiento me tiene mal, en el colegio la trigonometría la usas para hallar ángulos y todo eso pero no para rotar objetos :cry:

Busqué en la red y en un par de segundos di con la respuesta pero el algoritmo tiene un problema... la rotación implica desplazamiento que daña el dibujo, es decir, no rota en su propio eje.

Éste es el código que estoy usando.

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
sub rotate {
    my ( $self, $ang, $canvas ) = @_;
    my ( $x0, $y0 ) = $self->{Center};
    my @sideX = @{$self->{x}};
    my @sideY = @{$self->{y}};

    for ( my $i = 0; $i < (@sideX / 2); $i += 2 ) {
        # $x0 and $y0 = center; ($x2, $y2) = point 1; ($x3, $y3) = point 2;
        my $x2 = int( cos($ang) * ($sideX[$i]   - $x0) - sin($ang) * ($sideY[$i]   - $y0) + $x0 );
        my $y2 = int( sin($ang) * ($sideX[$i]   - $x0) + cos($ang) * ($sideY[$i]   + $y0)       );
        my $x3 = int( cos($ang) * ($sideX[$i+1] - $x0) - sin($ang) * ($sideY[$i+1] - $y0) + $x0 );
        my $y3 = int( sin($ang) * ($sideX[$i+1] - $x0) + cos($ang) * ($sideY[$i+1] + $y0)       );
        $canvas->line( $x2, $y2, $x3, $y3 );
    }
}
Coloreado en 0.003 segundos, usando GeSHi 1.0.8.4

NotaPublicado: 2008-06-22 21:02 @918
por explorer
Buen entretenimiento...

Solo un detalle inicial: en el juego original de asteroids lo único que rotaba era la nave del jugador.

En cuanto al procedimiento... debes realizar lo siguiente (una de las muchas soluciones).

* En algún lugar tienes definidas las coordenadas de dibujo de la nave, relativas al centro de la propia nave, que serán las coordenadas (0,0) y se supone que será también el centro de giro.

* A medida que la nave se mueve por la pantalla, llevas el control de su centro de giro (te sirve además para computar las colisiones con el resto de los objetos). Supongamos que sean ($nave_x, $nave_y).

* Cuando el usuario desea rotar la nave, incrementas o decrementas el valor de ese giro, en el ángulo de giro de la nave. Sea ($nave_a) ese ángulo.

* A la hora de pintar la nave, el procedimiento entonces es:

1. Partiendo de las coordenadas originales de definición de la nave, obtienes las nuevas coordenadas, giradas un ángulo $nave_a. Las fórmulas a aplicar son las indicadas por ti, salvo que puedes eliminar toda referencia a ($x0, $y0), ya que las coordenadas de las que partes son (0,0) (el centro de giro), por lo que no es necesario tomarlas en cuenta. Todas las coordenadas van a girar con respecto a ese centro, y como está en el (0,0), las fórmulas se simplifican.

2. Ya teniendo las coordenadas de los puntos de la nave, giradas, hay que trasladarlas al punto de dibujo final. Como el centro de giro de la nave en juego es ($nave_x,$nave_y), resulta que esto se traduce en sumar esas coordenadas de ese centro a cada una de las coordenadas de la nave.

El resultado es la nave, girada, en un punto del espacio 2D. Estos dos pasos los puedes realizar en un solo bucle, ya que el cálculo del "desplazamiento" de las coordenadas de la nave girada en el centro (0,0) no es afectada por la rotación.

Quedaría:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
$x1[$i] = int( cos($nave_a) * $x[$i] - sin($nave_a) * $y[$i] ) + $nave_x;
$y1[$i] = int( sin($nave_a) * $x[$i] + cos($nave_a) * $y[$i] ) + $nave_y;
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


Si estuviéramos en clase de álgebra, diríamos que hemos hecho una traslación, un giro, y una nueva traslación.

En un sentido purista, eso es lo que habría que hacer: a partir de las coordenadas de la nave en su situación espacial en un momento dado, girarla (primero traiéndola al (0,0), girando, y luego desplazándola a su posición final, o segundo, girándola directamente en su posición final 'si' sabemos cuales son las coordenadas del centro de giro). El problema con este planteamiento es que estamos trabajando con coordenadas enteras (posiciones de pixel), pero las operaciones trigonométricas, no. Poco a poco, y debido a la pérdida de la precisión en cada movimiento o giro, la nave se irá deformando. Podríamos evitándolo guardando las coordenadas con un montón de decimales, en lugar de valores enteros, pero al final, siempre hay errores de redondeo.

Pero este es un caso especial: la nave siempre es la misma y queremos que siempre mantenga su aspecto. Por eso giramos siempre a partir de las coordenadas originales de la nave. Eso nos asegura una apariencia idéntica en todas las situaciones y ángulos de rotación.

Una vez resuelto el problema, aparece la necesidad de mejorar el rendimiento: las operaciones trigonométricas son muy costosas para la CPU. Una solución es cambiar cálculo por espacio. Si suponemos que hacemos incrementos de ángulo de 10°, eso significa que solo necesitamos almacenar 35 juegos de coordenadas de dibujo de la nave. Eso lo podemos calcular muy rápido al principio del programa, con lo que, durante el juego, no necesitaremos hacer ningún cálculo trigonométrico: nos bastará con extraer el juego de coordenadas correspondiente al ángulo de giro de la nave en ese momento.

P.D. Las operaciones matemáticas en Perl necesitan ser expresadas en radianes. Puedes usar la función deg2rad() del módulo Math::Trig para hacer la conversión en el caso de que quieras trabajar con grados.

NotaPublicado: 2008-06-23 18:43 @821
por creating021
¡Muchas gracias por el comentario!
La idea de calcular las coordenadas al principio del programa es excelente, nuevamente y como siempre... muchas gracias.

NotaPublicado: 2008-08-22 19:36 @858
por creating021
Siguiendo el juego.... aún no he podido obtener las rotaciones :(
Éste es el código que genera los puntos:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
sub gen {
    my @x = ( 0, 5,  0, 5,  10, 10 );
    my @y = ( 0, 14, 0, 14, 0,  1  );
    my %ret = ( 0 => { "x" => @x, "y" => @y } );
    foreach my $ang ( 0 .. 358 ) {
        my ( @cordsX, @cordsY );
        my $num = ( 3.14159265358979 * $ang ) / 180;
        for ( my $i = 0 ; $i <= ($#x) ; $i += 2 ) {
            my $x2 = int( (cos($num) * $x[$i]) - (sin($num) * $y[$i]) );
            my $y2 = int( (sin($num) * $x[$i]) + (cos($num) * $y[$i]) );
            my $x3 = int( (cos($num) * $x[ $i + 1 ]) - (sin($num) * $y[ $i + 1 ]) );
            my $y3 = int( (sin($num) * $x[ $i + 1 ]) + (cos($num) * $y[ $i + 1 ]) );
            push @cordsX, ( $x2, $x3 );
            push @cordsY, ( $y2, $y3 );
        }
        $ret{$ang}->{x} = \@cordsX;
        $ret{$ang}->{y} = \@cordsY;
    }
    return %ret;
}
 
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


¿El problema está en esa función o lo estoy haciendo como es?

NotaPublicado: 2008-08-23 14:01 @626
por creating021
Creo que el error está en la función que lo genera, ya que al redondear el número crea "distorsiones".

He hecho este código, está interesante:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
#!/usr/bin/env perl
use strict;
use Image::XPM;
use POSIX qw(floor);

$| = 1;

my $img = Image::Xpm->new(-file => 'itest.xpm');
my $Y = $img->get(-width);
my $X = $img->get(-height);
my $new = Image::Xpm->new(-width => 300, -height => 300, -file => 'exit.xpm');
my $skip = '#ffffff';

my $ang = ( 3.14159265358979 * 30 ) / 180;

print $ang, "\n";

print "Blank the new img...";
foreach my $a ( 0.. 300 ) {
    foreach my $b ( 0 .. 300 ) { $new->xy($b, $a, $skip); } # blank the img;
}
print "OK\n";

for ( my $py = 0; $py <= $Y; $py++ ) {
    for ( my $px = 0; $px <= $X; $px++ ) {
        my $cl = $img->xy($px, $py);
        next if $cl eq $skip;
        my $nx = floor ( (cos($ang) * $px ) - ( sin($ang) * $py ) );
        my $ny = floor ( (sin($ang) * $px ) + ( cos($ang) * $py ) );
        #print "$nx $ny\n";
        $new->xy($nx +100, $ny +100, $cl);
    }
}

$new->save();
print "Done\n";
system("display exit.xpm");
Coloreado en 0.002 segundos, usando GeSHi 1.0.8.4


Uso floor, de POSIX es mejor que int.
itest.xpm es de 100x100 (en blanco y negro) que tiene un cuadrado grande en el centro.

Sí rota, pero miren el resultado... :lol:

NotaPublicado: 2008-08-23 14:42 @654
por explorer
Hay un error en la línea
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
my %ret = ( 0 => { "x" => @x, "y" => @y } );
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

Es muy sutil, el fallo... parece que es correcto: declaramos y definimos un hash, que tiene una clave ('0') que como valor tiene otro hash, con claves 'x' e 'y', con valores, cada una de ellas, un array de valores (@x e @y).

Pues no... no es eso lo que tenemos...

Si hacemos un Dumper de %ret, después de esa línea, obtenemos:
Código: Seleccionar todo
$VAR1 = {
          '0' => {
                   '0' => 1,
                   '10' => 'y',
                   'x' => 0,
                   '5' => 10
                 }
        };
que, desde luego, no se parece en nada, a lo que queríamos.

Bueno, el problema es que los array, normalmente, se 'despliegan' siempre que tienen oportunidad (por aquello de que, por defecto, se puede evaluar en contexto lista. Si no fuera así, estaría en contexto escalar, y nos devolvería el número de elementos).

Así que la línea anterior, en realidad, quiere decir esto:
Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
my %ret
    = (
        0 => {
            "x" => 0,
             5  => 0,
             5  => 10,
             10 => "y",
             0  => 14,
             0  => 14,
             0  => 1
        }
    );
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

¡Wow! ¡Vaya estropicio! Los elementos se van intercalando, dos a dos, y van formando el hash. Pero no de la forma que queríamos.

Y lo que queríamos era que esos array, en realidad, fueran un array anónimo, con los elementos del array dentro.

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
my %ret = ( 0 => { "x" => [@x], "y" => [@y] } );
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4


Aquí, indicamos claramente que los valores correspondientes a "x" e "y" son dos array anónimos. Y los array @x e @y... hacen su trabajo: desparramarse. Aunque ahora, con contención.

Código: Seleccionar todo
$VAR1 = {
          '0' => {
                   'y' => [
                            0,
                            14,
                            0,
                            14,
                            0,
                            1
                          ],
                   'x' => [
                            0,
                            5,
                            0,
                            5,
                            10,
                            10
                          ]
                 }
        };


¡Pero qué útil es Data::Dumper!

NotaPublicado: 2008-08-23 16:00 @708
por creating021
¡Ignoraba ese dato! Ahora rota mucho mejor con ese detalle, además, tenía otro problema en la función que dibuja el triángulo.

Muchas gracias.

PD: En cuanto a la imagen, es cuestión de redondeo y no del código en sí.

NotaPublicado: 2008-08-23 17:40 @778
por explorer
Esta es mi variación.

Sintáxis: [ Descargar ] [ Ocultar ]
Using perl Syntax Highlighting
#!/usr/bin/perl
use strict;
use warnings;
use diagnostics;

use POSIX 'floor';

## Coordenadas originales
my @x = ( 0,  5,  0,  5, 10, 10 );
my @y = ( 0, 14,  0, 14,  0,  1 );

## Coordenadas precalculadas
my @coordenadas;

## Generador de coordenadas
sub genera_coordenadas {

    for (my $angulo = 0; $angulo < 359; $angulo += 1  ) {
        my $a_deg = (3.14159265358979 * $angulo) / 180;

        for (my $i  = 0;      $i < @x;  $i++          ) {
            $coordenadas[$angulo][$i] = [
                floor( cos($a_deg) * $x[$i] - sin($a_deg) * $y[$i] ),
                floor( sin($a_deg) * $x[$i] + cos($a_deg) * $y[$i] ),
            ];
        }
    }
}

## Programa
genera_coordenadas();

use Data::Dumper;
print Dumper(\@coordenadas);
Coloreado en 0.001 segundos, usando GeSHi 1.0.8.4

Guardando las coordenadas de esta manera, se pueden obtener las coordenadas de forma parecida. Se trata de un array de array en lugar de un hash de hash de array, como tenías.

Ejemplo, las coordenadas para el ángulo 30 son: @{$coordenadas[30]}. Y cada coordenada es un array con 2 componentes, la de 'x' y la de 'y'. Luego, la componente 'y' de la primera coordenada, a 30 grados, es $coordenadas[30][0][1].

El guardar los datos de esta manera, tiene una ventaja: puedes modificar la forma de la nave en mitad del juego, con solo variar @x e @y y volviendo a generar_coordenadas().

Esto para vectorial... Si estás pensando en bitmap, casi mejor echar un vistazo al código de Frozen Bubble, y entrar en el terreno del SDL. Un poco de paciencia, que son más de 5000 líneas de Perl :)