Tutoblog
Página Web personal de mis proyectos u otros haberes
/// Estructura previa a la memoria
struct Head
{
/// Tamaño
size_t size;
/// Siguiente
Head* next;
};
Esta estructura lo que hará es memorizar el tamaño y un puntero a la siguiente memoria, el motivo es que debemos
de memorizar varias memorias dentro de una misma posición en la tabla hash./// Obtener el puntero a una zona de memoria del tamaño indicado
/// size = Tamaño de la memoria
/// Retorna Puntero a la memoria
static void* New( size_t size )
{
Head* ret;
// Calcular el índice en la tabla hash
size_t index = size % capacity;
Head* previous = NULL;
ret = hashTable[index];
while( ret != NULL )
{
// Si tienen el mismo tamaño nos vale
if( ret->size == size )
{
if( previous == NULL )
{
// Es el primero de la lista
hashTable[index] = hashTable[index]->next;
}
else
{
// Es un por el medio de la lista
previous->next = ret->next;
}
#ifdef dib_SeeMemoryLeaks
countMemUsed++;
#endif
// Retornar memoria
return (void*)(ret + 1);
}
// Siguiente
previous = ret;
ret = ret->next;
}
// No existe ninguno de ese tamaño, creamos uno nuevo
ret = (Head*)malloc(sizeof(Head) + size);
if( ret != NULL )
{
// Indicar el tamaño, solo se hará esta vez
ret->size = size;
#ifdef dib_SeeMemoryLeaks
countMemUsed++;
#endif
// Retornar memoria
return (void*)(ret + 1);
}
// No se pudo obtener una memoria de dicho tamaño
return NULL;
}
La función aunque parece complicada es muy sencilla, primero calcula el
índice de la tabla hash mediante el
modulo, mira si existe un bloque de memoria en esa posición de la tabla hash, si es
así mira a ver si el tamaño
de ese bloque de memoria es el deseado, si no es así, pasa al siguiente bloque de memoria en la lista, cuando
encuentra un bloque de memoria del tamaño deseado directamente lo retorna. Si en la
posición de la tabla hash
no existe ningún bloque de memoria entonces crea uno nuevo del tamaño indicado mas el tamaño de la estructura de
la cabecera./// Borrar una zona de memoria obtenida mediante la función New de esta clase
/// pointer = Puntero a la memoria que se quiere borrar
static void Delete( void* pointer )
{
if( pointer != NULL )
{
#ifdef dib_SeeMemoryLeaks
countMemUsed--;
#endif
// Insertarlo en la lista
Head* head = GetHead( pointer );
size_t index = head->size % capacity;
head->next = hashTable[index];
hashTable[index] = head;
}
}
Lo único que hace esta función es meter el bloque de memoria que queremos liberar a la tabla hash.
Lo primero que haremos será descargar el Java(TM) 2 SDK, Standard Edition 1.4.2_12 (j2sdk-1_4_2_12-windows-i586-p.exe), si encuentra algún problema en el link pueden realizar la búsqueda directamente desde google indicando ese nombre del archivo, una recomendación es que dejemos todos los directorios por defecto ya que algunos instaladores no soportaran espacios y pueden surgir incompatibilidades.
Una vez instalado tendremos el runtime de java y el SDK instalados correctamente, en alguna versión anterior a Windows XP puede que haga falta reiniciar, es recomendable no saltarse ni un solo reinicio para el correcto funcionamiento.
Ahora procederemos a instalar el Netbeans (netbeans-5_0-windows.exe), si hemos hecho bien el paso anterior no tendremos ningún problema durante la instalación, además podemos dejarlo todo por defecto, la instalación puede ser algo lenta así que si ves la barra que se detiene no le des a cancelar.
Una vez instalado procederemos a instalar su plugin para j2me llamado Netbeans Mobility Pack 5.0 (netbeans_mobility-5_0-win.exe), la instalación es rapidísima y muy sencilla.
Ahora solo nos queda instalar los sdks, comenzaremos por los de Nokia, antes de poder descargar algún SDK de la Web de Nokia debemos de registrarnos, una vez registrados ya podemos proceder a descargar los sdks, para empezar instalaremos el SDK para serie 40 (3rd Edition Platform FP 1), es el que muestra la siguiente imagen:

Durante la instalación puede que nos de algunos errores, pero podemos ignorarlos sin problema, ahora una vez instalado haremos lo mismo para el sdk serie 60 (SDK for 3rd Edition), deberemos de seleccionar la instalación "Custom":

y marcar la casilla "Multiple Emulator Support", de esta manera se instalaran varios emuladores diferentes con los que podremos probar nuestra aplicación o juego en j2me desde Netbeans.

Una vez concluida la instalación procederemos a la instalar el sdk para los móviles de la serie 80 (J2ME(TM) MIDP), la instalación de este es un poco mas liosa ya que nos pide un serial que nos llegara por correo, el primer paso es saltarnos este paso dándole a "Next" sin rellenar ningún campo:

Ahora cuando lleguemos al paso de la siguiente imagen deberemos de introducir nuestro nombre de usuario y contraseña:

Al poco rato de haberle dado al botón "Next" nos llegara al correo que indicamos al crear la cuenta el numero de serie que debemos de introducir en esta pantalla:

Oculto el correo y el numero de serie para no incumplir cualquier cosa que exista en la licencia, durante la instalación puede que de algún error, realmente no pasa nada y podemos seguir instalándolo todo con tranquilidad.
Ahora pasaremos a instalar el sdk de Motorola (Motorola J2ME(TM) SDK v6.11 for Motorola OS Products), la instalación no podría ser mas sencilla, lo único si no tenemos instalado el QuickTime nos saldrá un mensaje diciendo que el audio no podrá ser emulado, lo podemos solucionar instalándolo antes o después (recomiendo instalar antes el QuickTime por si acaso).
Procederemos a instalar el sdk de Sony Ericsson (Sony Ericsson SDK 2.2.4 for the Java(TM) ME Platform), cuando nos salga esta pantalla le indicaremos que "SI":

Ahora nos toca instalar el sdk de Siemens, pero para ello deberemos de registrarnos previamente, después de registrarnos nos llegara un correo electrónico indicándonos nuestra clave de acceso, ahora ya podemos descargarnos el sdk de Siemens (V3.2.623 65/75 Generation Core), la instalación no tiene ningún misterio, dejaremos todo por defecto. (Gracias a chechocossa miembro de stratos-ad por pasarme el link). Aparte de instalar el sdk debemos de descargarnos los emuladores, las paginas Web de donde se puede descargar son estas: 75/85 Generation, 65/70 Generation, 45/50/55/60 Generation y SX1. La instalación de los emuladores es un simple doble click, por lo menos debemos de instalar un emulador.
Una vez instalados todo esto nos toca irnos al Netbeans y configurarlo para que el IDE reconozca todas las plataformas, este paso realmente sencillo si se ha hecho todo lo anterior correctamente, lo primero será ejecuta el Netbeans desde el menú inicio o desde el escritorio, una vez que este funcionando deberemos dirigirnos al menú "Tools" y seleccionar "Java Platform Manager":

Nos mostrara una pantalla como esta:

Ahora solo toca agregar las plataformas de las diferentes marcas de móviles, para ello presionaremos el botón "Add Platform...", al realizar esto se nos mostrar una pantalla como la que sigue en la que seleccionaremos la casilla "Java Micro Edition Platform Emulator" y luego presionaremos el botón "Next:

Llegados a este paso mágicamente el Netbeans a encontrado todos los sdks que hemos instalado, si por algún casual faltara alguno tendremos que agregarlo indicándole el directorio donde se encuentra presionando el botón "Find more Java ME Platform Folders...":

Dejaremos al Netbeans trabajar ya que se pondrá a detectar todas las plataformas, una vez que termine nos mostrara esta pantalla, deberemos dar al botón "Finish":

Ahora ya tenemos todas las plataformas configuradas, me toca mostraros como crear un proyecto que use todas las plataformas, para ello vamos al menú "File" y seleccionamos "New Proyect", se nos mostrara un dialogo como este en el que seleccionaremos "Mobile" y "Mobile Application":

Cuando se nos muestra la pantalla de a continuación seleccionaremos la casilla de "Select All" y presionaremos el botón "Finish":

Llegados a este punto el Netbeans nos habrá creado un proyecto con múltiples configuraciones, estas pueden verse en las propiedades del proyecto, dando el botón derecho sobre el nombre del proyecto, el dialogo que se muestra será parecido a este:

Como se ve en la imagen anterior tenemos todas las plataformas listas y configuradas, ahora si queremos compilar para todas las plataformas al mismo tiempo deberemos ir al menú "Build" y seleccionar "Build All Main Project Configurations" o la opción seleccionada en la imagen.

Compilar para todas estas plataformas puede llevar mucho tiempo así que mientras se trabaja con la aplicación o juego lo mejor es compilar con "Build Main Project" (F6) de esta manera solo se compilara para la plataforma que este seleccionada:

Llegados a este punto solo me queda por explicar como diferenciar una y otras plataformas, actualmente la mayoría de los APIS como los de Nokia ponen que están obsoletas y que uses el API nativa de MIDP2.0, con lo que nos facilita mucho las cosas, no obstante existe la posibilidad de saber que MIDP esta siendo usando y además ya viene configurado en cada plataforma, el truco se llama "Abilities" y se encuentra en las propiedades del proyecto:

Como se ve en la imagen anterior en el aparatado "Abilities" podemos definir constantes, todas las plataformas tienen definido la constante "MIDP" a valor "1.0" o "2.0", ¿pero como sabes desde el código estos valores? básicamente se hace exactamente igual que el precompilador de C++:

En la imagen se muestra claramente como acceder a estos valores "Abilities" desde código, todos los valores son cadenas de caracteres con lo que tienen que estar entrecomillados, pero sus variables no, además por ejemplo Nokia tiene mas constantes que nos pueden ayudar, como la constante NOKIAUI que nos indica que la plataforma destino es Nokia.
Bueno pues básicamente ya puede programar aplicaciones para diferentes móviles con un par de clicks de ratón que era el objetivo de este articulo, ya solo le queda aprender j2me si no lo sabe y aprovechar las ventajas que puede llegar a dar los APIS de cada móvil, no obstante existen diversas utilidades que nos facilitan mucho el trabajo, entre ellas la mas conocida y usada a mi parecer es el J2ME Polish que viene con una base de datos con montones de configuraciones para móviles.
Si has logrado echarlo todo a funcionar y haber leído hasta aquí solo
te puedo decir: SUERTE!

Como todos sabéis los juegos normalmente siempre tienen una presentación, una parte donde se juega, tabla de puntuaciones, etc. a nivel de programación debemos de separar estas partes en algo que se llaman estados de aplicación, un estado de aplicación es llamándolo de forma muy directa un objeto que recibe eventos de otro objeto que conoce como funciona un estado. El principal motivo de separar las partes de un juego es crear el código estrictamente necesario para ese momento del juego, además con esto se gana velocidad y limpieza.
Comenzare con definir como es un estado, lo haré en lenguaje C++, pero es aplicable para otros lenguajes, al final del articulo mostrare la implementación en mas lenguajes.
class State
{
public:
// Evento al seleccionar el estado
virtual bool OnSelect() = 0;
// Evento al deseleccionar el estado
virtual bool OnUnSelect() = 0;
// Mas eventos y/u otras cosas
...
}
Como se ve he definido dos eventos virtuales puros, el evento "OnSelect" se
llamara cuando el estado vaya a ser activado o seleccionado, el "OnUnSelect"
cuando el estado vaya a dejar de usarse, el primero vale para crear objetos,
inicializar valores, etc. mientras que el segundo nos sirve para liberarlos.Ahora pasaremos a definir la clase que trabajara con los estados.
class StateMachine
{
public:
// Constructor
StateMachine()
{
this->state = this->newState = NULL;
}
// Destructor
~StateMachine()
{
this->SetState( NULL );
this->UpdateStateMachine();
}
// Cambiar de estado
void SetState( State *state )
{
this->newState = state;
}
// Obtener el estado actual
State *GetState()
{
return this->state;
}
// Mirar si se debe de cambiar de estado en este ciclo
void UpdateStateMachine()
{
if( this->state != this->newState )
{
if( this->state != NULL )
{
this->state->OnUnSelect();
delete this->state;
}
this->state = this->newState;
if( this->state != NULL )
{
this->state->OnSelect();
}
}
}
private:
// Estado actual
State *state;
// Nuevo estado
State *newState;
}
Como se vera en el código se hace algo extraño y es que cuando queremos cambiar de estado no se hace inmediatamente, el motivo es dar la posibilidad de definir un estado en cualquier parte del estado actual sin que se ocasionen problemas, es decir, si cambiáramos de estado inmediatamente y luego se ejecutara algo de código la aplicación daría un error ya que ese estado a dejado de existir y de estar seleccionado, la función "UpdateStateMachine" permite definir en que parte del código queremos que se produzca este cambio, normalmente esta clase se llamara desde el núcleo de un motor o el bucle principal de la aplicación con lo que esta función debe de ser llamada en una parte en el que el código sea seguro, por ejemplo:
int main()
{
...
while( !exit )
{
// actualizar
...
// renderizar
...
// Cambiar de estado si es necesario
machine->UpdateStateMachine();
}
Como se ve la función "UpdateStateMachine" se ejecuta al final del bucle principal, gracias a esto tenemos la certeza de que el cambio de estado no producirá ningún caso extraño que cuelgue nuestro juego o aplicación.
Una vez definidas estas dos clases ya podemos separar nuestro juego en partes bien definidas con solo derivar la clase State, pero en C++ existe un pequeño problema y es que un estado puede llamar a otro, es decir, A puede llamar a B, pero B puede llamar a A, y es imposible poner los includes en A y B, con lo que la solución son los templates, mejor veis el código:
class StateMachine
{
...
// Cambiar de estado
template <class STATE> void SetState()
{
this->newState = new STATE;
}
...
}
Con este pequeño código podemos cambiar de estados con solo pre declarar los
estados a los que queremos cambiar, este es el motivo de por que en el constructor un estado no se debe de inicializar
nunca y para ello se debe de usar el evento "OnSelect", un ejemplo:
// staterun.h
class StateGame;
class StateRun : public State
{
void OnSelect()
{
// Como la aplicación solo puede tener
// un estado lo correcto es hacer una
// maquina de estados global
gMachine.SetState<StateGame>();
}
...
}
// stategame.h
class StateRun;
class StateGame : public State
{
void OnSelect()
{
gMachine.SetState<StateRun>();
}
...
}
Como se ve en el código con pre declarar la clase StateGame encima de StateRun el código funcionara correctamente y lo mismo sucede con StateRun, el principal motivo de usar aquí un template es que muchos estados pueden ser templates y que no tengan un archivo .cpp.
Como prometí aquí esta el código para java:
// Clase base para los estados
public abstract class State
{
// Se activo este estado
public abstract void onSelect();
// Se desactivo este estado
public abstract void onUnSelect();
// ...
}
// Maquina de estados
public class StateMachine
{
// Cambiar de estado
public void setState( State state )
{
this.newState = state;
}
// Obtener el estado
public State getState()
{
return this.state;
}
// Cambiar de estado si es necesario
public void updateStateMachine()
{
if( this.state != this.newState )
{
if( this.state != null )
{
this.state.onUnSelect();
}
this.state = this.newState;
if( this.state != null )
{
this.state.onSelect();
}
// Limpiar el recolector de basura
// (no es necesario, pero esta bien hacerlo)
System.gc();
}
}
// Estado actual
private State state;
// Futuro estado
private State newState;
}
Para java no hace falta nada de templates ni trucos de este tipo ya que las clases estando en un mismo paquete se conocen y si no se pueden importar sin problemas, además debemos de notar que el evento onUnSelect será rara vez usado ya que los objetos se liberan de forma inmediata al asignar el this.state a this.newState.
En este articulo intentare hacer que los programadores vean su código compilado en la cabeza según lo están escribiendo, al principio les puede parecer complicado pero después de algún tiempo haciéndolo no lo es en absoluto. Para ello vamos a deducir como un compilador trata las secuencias if y como debemos de hacerlo correctamente para ganar velocidad. Tenemos este código:
struct Elemento
{
void *dato;
};
Elemento *Mete( void *dato )
{
if( dato == NULL )
{
return NULL;
}
Elemento *nuevo = new Elemento;
if( nuevo == NULL )
{
return NULL;
}
nuevo->dato = dato;
return nuevo;
}
Esta función mete un dato dentro de una estructura Elemento, es una función
sin sentido pero que nos vale para lo que queremos hacer. Como vemos este más
o menos es el código que harían la mayoría de los programadores por comodidad,
exceptuando que muchos quitarían las llaves, yo las dejo ya que vale para explicar
mejor a donde quiero llegar, si examinamos el código veremos que si el dato
pasado a la función es null queremos que de un error y salga, después creamos
el elemento y si este null saldrá dando un error, si todo sale bien dato se
almacena dentro de la estructura y esta es devuelta para su manejo, ahora les
muestro el código que seria correcto para ganar velocidad:
struct Elemento
{
void *dato;
};
Elemento *Mete( void *dato )
{
if( dato != NULL )
{
Elemento *nuevo = new Elemento;
If( nuevo != NULL )
{
nuevo->dato = dato;
return nuevo;
}
}
return NULL;
}
Como se ve la lógica del código es contraria totalmente a la anterior favoreciendo
a los casos en que todo sea correcto, es decir, diferente a NULL, ¿y cual es
el motivo de esto? el motivo es que todos los compiladores por lógica siempre
que se entra dentro del if no se produce un salto, en cambio si el if no se
cumple si que se produce, el motivo es que cuando el contenido de un if termina
se tiene que seguir haciendo lo que va después, veamos lo que digo en un pseudo
código:
comparar dato con NULL
es diferente?
{
… código dentro de las llaves
}
… código que va después
Como se ve si es diferente se hace el contenido de las llaves, pero sea o
no diferente el código que va después siempre se hace, entonces llegamos a la
conclusión de que el compilador producirá un salto para el caso contrario que
pongamos nosotros, ya que si no el compilador realizara un salto al contenido
de las llaves y después al final de estas otro salto a lo que va después de
ellas, en conclusión, el compilador optimiza poniendo un salto a la comprobación
contraria a la que ponemos nosotros, así solo produce saltos en el caso que
no se cumpla lo que indicamos y siempre que se cumpla el código va secuencialmente,
además de que al final de las llaves no se producirá un salto ya que al producirse
solo cuando no se cumple el código esta justo debajo de las llaves. Ahora examinando
el primer código que puse veremos que estamos produciendo un salto siempre y
en el segundo código se ve que solo se producirá el salto cuando las condiciones
no se cumplan, esto también nos lleva a deducir en que siempre debemos de favorecer
al código que se cumpla mas del 50% de las veces, es decir, en el segundo código
vemos que lo normal es pasar un dato que no sea nulo y que la mayoría de las
veces exista suficiente memoria como para crear una estructura Elemento, de
esta manera no existirán saltos en el código, exceptuando claro esta el retorno
de nuevo o null. Mas beneficios de este sistema de programación son códigos
como este:
bool CrearMalla( int numVertices, int numIndices, char *fileShader )
{
VertexBuffer *vertices = new VertexBuffer( numVertices );
if( vertices != NULL )
{
IndexBuffer *indices = new IndexBuffer( numIndices );
if( indices != NULL )
{
Shader *shader = new Shader( fileShader );
if( shader != NULL )
{
return true;
}
delete indices;
}
delete vertices;
}
return false;
}
Viendo la lógica nos damos cuenta que siempre que se haga todo correcto no
se producirán saltos, si ocurre algún error se produce un solo salto y destruimos
los objetos en el orden inverso de su creación.
La mayoría de las veces la gente cuando se pone a programar juegos 2d o 3d se centran exactamente en el numero de dimensiones que tendrá ese juego, esto en mi humilde opinión es una forma de pensar totalmente incorrecta, el problema de pensar en 2d o 3d es que creamos funciones para este tipo de dimensiones y no pensamos en la verdadera optimización de pensar en n-dimensiones. ¿Que es pensar en n-dimensiones? Pensar en n-dimensiones es tener claro el concepto de que es una dimensión, para explicar lo que es una dimensión lo haré de la forma mas sencilla que conozco, una dimensión no es mas que un espacio comprendido entre dos puntos que se llaman -infinito y +infinito, en un ordenador lamentablemente infinito no existe (salvo en bucles), la mayoría de las veces las dimensiones son tratadas de -float +float, es decir, de 1.17549e-38 hasta 3.40282e+38, teniendo claro esto, podemos decir que todas las dimensiones se comportan de la misma manera ya que solo definen la posición en un espacio (algunos lo entenderéis mejor si lo llamara vector). Un ejemplo practico de como pensar en n-dimensiones es la clásica función para comprobar si un cubo definido por dos puntos (mínimo y máximo) esta dentro de otro o por lo menos esta parcialmente dentro de otro, la mayoría de las personas harán la clásica rutina de mirar si uno de los dos puntos del cubo A esta dentro de los puntos del cubo B, esto en mi opinión es un error muy gordo ya que solo se ha pensado en 3d y no en n-dimensiones, partimos de que tenemos las siguientes estructuras:
struct Vector
{
float x, y, z;
}
struct Caja
{
Vector min, max;
}
El código pensado para 3d seria algo así:
bool Colision3D( Caja& caja1, Caja& caja2 )
{
if( PuntoDentro3D(caja1, caja2.min) || PuntoDentro(caja1, caja2.max) )
{
return true;
}
return false;
}
bool PuntoDentro3D( Caja& caja1, Vector& punto )
{
if( punto.x > caja1.min.x && punto.x < caja1.max.x &&
punto.y > caja1.min.y && punto.y < caja1.max.y &&
punto.z > caja1.min.z && punto.z < caja1.max.z )
{
return true;
}
return false;
}
Vemos que comprobamos que uno de los dos puntos de la caja B se encuentre dentro
de la caja A, esto genera 12 comprobaciones, es decir 6 por cada punto, ¿que
pasaría si hiciéramos esta función pensando en n-dimensiones? Antes de dar la
solución es mejor explicar paso a paso como llegar a ella. Lo primero es quitar
las dimensiones Y y Z del tablero de juego, es decir, nos quedamos solo con
la dimensión X que se comporta de la misma manera que Y y Z, ahora pensamos
lo siguiente, ¿que debemos de hacer para comprobar si un valor se comprende
entre otros dos, por ejemplo entre el 10 y el 20?
if( valor > 10 && valor < 20 ) return true;Si el valor es mayor a 10 y el valor es menor a 20 es que valor esta entre 10 y 20 lógicamente, ahora un poco mas complicado, ¿que hacemos para saber si min y max se comprende entre 10 y 20 o esta parcialmente dentro?
if( max > 10 && min < 20 ) return true;¿Ein? ¿y como es que con las mismas comprobaciones logramos esto? Pues muy sencillo, debemos de mirar todos los casos posibles, si máximo es menor a 10, significa que min es menor a 10 ya que max es mayor siempre que min, si max es mayor a 10 o 20 significa que y min y max pueden estar dentro de 10 o 20, si min es mayor a 20 significa que esta fuera del rango de 10 y 20 ya que max siempre es mayor a min, si es menor significa que puede estar dentro del rango de 10 y 20, entonces llegamos a la conclusión de que si max es mayor a 10 y min es menor a 20 esta dentro, también se podría ver al revés, decir que si min es mayor a 20 o max es menor 10 no estaría dentro.
if( min > 20 || max < 10 ) return false;Teniendo esta afirmación en la dimensión X podemos decir que todas las demás dimensiones la cumplen ya que se comportan igual, con lo que comprobar un cubo con valor máximo y mínimo dentro de otro seria de esta manera:
bool ColisionNDimensiones( Caja& caja, Caja& caja2 )
{
if( caja1.min.x < caja2.max.x && caja1.max.x > caja2.min.x &&
caja1.min.y < caja2.max.y && caja1.max.y > caja2.min.y &&
caja1.min.z < caja2.max.z && caja1.max.z > caja2.min.z )
{
return true;
}
return false;
}
Nos hemos ahorrado 6 comprobaciones, esta entre otras es una de las ventajas
de pensar en n-dimensiones, ¿que ocurría si quisiéramos crear comprobaciones
con cubos de mas dimensiones que 3, por ejemplo 5? Seria agregar comprobaciones,
algo así:
bool ColisionNDimensiones( Caja& caja, Caja& caja2 )
{
if( caja1.min.x < caja2.max.x && caja1.max.x > caja2.min.x &&
caja1.min.y < caja2.max.y && caja1.max.y > caja2.min.y &&
caja1.min.z < caja2.max.z && caja1.max.z > caja2.min.y &&
caja1.min.a < caja2.max.a && caja1.max.a > caja2.min.a &&
caja1.min.b < caja2.max.b && caja1.max.b > caja2.min.b)
{
return true;
}
return false;
}
Como veis mientras mas dimensiones mas comprobaciones nos ahorramos. Espero
que ahora estéis todos pensando en n-dimensiones