Gráficos.
Gráficos... Veremos las herramientas que nos brinda Slick para graficar. Existe todo un paquete dedicado a gráficos llamado org.newdawn.slick.geom. Con él se pueden hacer las figuras tradicionales, tales como, círculos, rectángulos, elipses, triángulos, líneas, etcétera.
Resumen: Dibujo de círculos, elipses y líneas. Fractal: Copo de nieve de Von Koch.
Dibujando...
Existen dos alternativas para hacer los dibujos. Las dos hacen uso de la clase Graphics del paquete org.newdawn.slick. Una opción usa solamente esa clase mientras que la otra alternativa es usar la clase pero ayudada del paquete org.newdawn.slick.geom.
Hay algunas diferencias en cuanto a los parámetros que reciben los métodos para dibujar. Usted puede optar por usar cualquiera de las dos alternativas.
Comenzaré por la segunda opción.
Recordando. Cuando crea una clase que herede de la clase BasicGame debe redefinir 3 métodos, entre ellos, está el método render(GameContainer c, Graphics g) que recibe dos parámetros. El segundo de ellos, es un objeto de la clase Graphics. Éste nos permitirá dibujar mediante su método:
draw(Shape figura)
y pintar mediante su método:
fill(Shape figura)
Lo que debemos hacer es crear las figuras (Shape) que queremos dibujar o pintar. Bien, Slick nos brinda algunas clases que nos permitan crear esas figuras. La clase Shape es una clase abstracta desde la cual derivan las clases:
- Circle
- Curve
- Ellipse
- Line
- MorphShape
- Path
- Point
- Polygon
- Rectangle
- RoundedRectangle
Todas estas clases están dentro del paquete org.newdawn.slick.geom.
Nuestra tarea es crear instancia de esas clases concretas. Es decir, si queremos un círculo pues creamos una instancia de la clase Circle. Si queremos una línea creamos una instancia de la clase Line.
La creación de las figuras es recomendable que se haga en el método init(), pues se crean una sola vez y luego las dibujamos, por supuesto, en el método render(). Para esto, por cada figura que quisiéramos graficar deberíamos tener un atributo. El siguiente programa muestra el esqueleto de lo dicho:
import org.newdawn.slick.*;
public class MiJuego extends BasicGame {
// Por cada figura crearemos un atributo.
private AppGameContainer contenedor;
public MiJuego() throws SlickException {
super("Gráficos");
contenedor = new AppGameContainer(this);
contenedor.setShowFPS(false);
contenedor.setDisplayMode(600, 500, false);
contenedor.start();
}
@Override
public void init(GameContainer container) throws SlickException {
// Aquí iría la creación de nuestras figuras.
}
@Override
public void update(GameContainer container, int delta) throws SlickException {
}
@Override
public void render(GameContainer container, Graphics g) throws SlickException {
// Aquí iría el dibujo de nuestras figuras.
}
}
Clase Circle.
La clase Circle ubicada dentro del paquete org.newdawn.slick.geom permite crear un círculo. Define dos contructores:
new Circle(float centroX, float centroY, float radio)
new Circle(float centroX, float centroY, float radio, int numSe)
Ambos constructores aceptan el punto central (x, y), recuerde que el inicio de las coordenadas son tomadas desde la esquina superior izquierda. Además, reciben el radio como tercer parámetro.
El segundo constructor se diferencia del primero por el cuarto parámetro:
int numSe
Este parámetro numSe es la cantidad de segmentos por el cual va a estar formado el círculo. Debe saber que un círculo en una computadora no es realmente un círculo sino que se forma por muchos segmentos rectos y pequeños. Entonces, este parámetro permite especificar la cantidad de esos segmentos. Vea a continuación las siguientes figuras:
El "círculo" de la figura 1 se creó con 5 segmentos, haciendo:
circulo1 = new Circle(50, 50, 20, 5)
El "círculo" de la figura 2 se creó con 7 segmentos, haciendo:
circulo2 = new Circle(100, 50, 20, 7)
El "círculo" de la figura 3 se creó con 11 segmentos, haciendo:
circulo3 = new Circle(150, 50, 20, 11)
Observe que usando 11 segmentos el círculo mejoró bastante.
Y por último, el círculo de la figura 4, se creó haciendo:
circulo4 = new Circle(200, 50, 20)
Es decir, no se le paso ningún número de segmentos (usamos el primer constructor). A modo de inquietud, la creación de un círculo sin pasar el número de segmentos se crea con 50 segmentos usando la constante estática protegida DEFAULT_SEGMENT_COUNT definida en la clase Ellipse. Como veremos en un momento la clase Ellipse es la clase padre de la clase Circle, pues esto es lógico ya que un círculo es una elipse.
A continuación se detalla el programa:
import org.newdawn.slick.*;
import org.newdawn.slick.geom.*;
public class MiJuego extends BasicGame {
// Por cada figura creamos un atributo.
private Circle circulo1;
private Circle circulo2;
private Circle circulo3;
private Circle circulo4;
private AppGameContainer contenedor;
public MiJuego() throws SlickException {
super("Gráficos");
contenedor = new AppGameContainer(this);
contenedor.setShowFPS(false);
contenedor.setDisplayMode(600, 500, false);
contenedor.start();
}
@Override
public void init(GameContainer container) throws SlickException {
circulo1 = new Circle(50, 50, 20, 5);
circulo2 = new Circle(100, 50, 20, 7);
circulo3 = new Circle(150, 50, 20, 11);
circulo4 = new Circle(200, 50, 20);
}
@Override
public void update(GameContainer container, int delta) throws SlickException {
}
@Override
public void render(GameContainer container, Graphics g) throws SlickException {
g.fill(circulo1);
g.fill(circulo2);
g.fill(circulo3);
g.fill(circulo4);
}
}
Además la clase Circle provee algunos métodos útiles:
- boolean contains(float x, float y): chequea si el punto está contenido por el círculo.
- float getCenterX(): devuelve la coordenada x del centro del círculo.
- float getCenterY(): devuelve la coordenada y del centro del círculo.
- float getRadius(): devuelve el radio del círculo.
- void setRadius(float rad): establece el radio del círculo.
- boolean intersects(Shape shape): devuelve verdadero si este círculo interseca con la figura pasada como parámetro.
Observe que llamando al método setRadius() de la siguiente manera:
circulo4.setRadius(circulo.getRadius() + 0.01f)
Se puede hacer que el círculo vaya creciendo. Pruébelo con escribirlo en el método update() del programa anterior y observe como el tamaño del círculo cambia pero... pero también cambian las coordenadas del centro del círculo, parece que el círculo crece pero que también se mueve. ¿Esperaba ese comportamiento o esperaba que el centro del círculo se quede fijo y que lo único que cambie sea el radio? Si lo que desea hacer es que el círculo quede fijo y que sólo varíe su tamaño (radio), puede escribir:
@Override
public void update(GameContainer container, int delta) throws SlickException {
circulo4.setRadius(circulo4.getRadius() + 0.01f);
circulo4.setCenterX(circulo4.getCenterX() - 0.01f);
circulo4.setCenterY(circulo4.getCenterY() - 0.01f);
}
Clase Ellipse.
Como mencioné anteriormente la clase Ellipse es padre de la clase Circle. Pero a su vez es hija de la clase Shape, lo que llevaría a una jerarquía de la siguiente forma:
Circle ======> Ellipse ======> Shape.
Esta clase define también dos constructores:
new Ellipse(float centroX, float centroY, float radio1, float radio2)
new Ellipse(float centroX, float centroY, float radio1, float radio2, int numSe)
Los constructores aceptan el punto central y los radios. El segundo constructor acepta, además, el número de segmentos para construir una elipse. Note que el radio 1 es el radio horizontal y el radio 2 es el vertical.
Métodos.
- float getRadius1()
- float getRadius2()
- void setRadius1(float rad1)
- void setRadius2(float rad2)
- void setRadii(float rad1, float rad2): establece los dos radios. Observe que el nombre del método termina con dos i.
Shape transform(Transform transform): este método permite transformar la elipse pudiendo rotarla, cambiarle la escala (operación de escalamiento) o trasladarla. Estudiaremos las transformaciones de imágenes más adelante.
Clase Line.
La clase Line, obviamente, también hereda de Shape. Esta clase define 6 constructores pero misteriosamente uno de ellos no funciona y si mira un poco el código por adentro notará inmediatamente el error. Siempre lanza un NullPointerException. Esperemos que en un futuro este error se arregle. Por supuesto usted puede modificar el código para que funcione bien (yo lo hice ;) ). A pesar de este error y de algún otro, la clase Line es bastante estable y funciona muy bien.
Constructores:
- new Line(float x, float y): construye una línea desde el origen (0,0) hasta el punto especificado (x, y).
- new Line(float x, float y, boolean inner, boolean outer):idéntico al constructor anterior, las variables boolean no se usan. Este es otro método que sorpresivamente no tiene ninguna relevancia. Parece que a la clase Line no se la tomaron muy en serio.
- new Line(float[] puntoInicial, float[] puntoFinal): Su propósito es crear una línea recta mediante dos puntos: el punto inicial y el punto final. Cada punto es representado por un arreglo de flotantes, obviamente, de longitud 2. Lamentablemente, este constructor es el que siempre lanza un NullPointerException.
- new Line(float x1, float y1, float x2, float y2): crea una nueva línea basada en dos puntos (punto inicial y punto final). Por suerte este método muy útilfunciona bien.
- new Line(float x1, float y1, float x2, float y2, booleandummy): crea una nueva línea que comienza en un punto (x1, y1) y a partir de ese punto la línea se extiende con un vector de coordenadas (x2, y2). La última variable dummy (tonta) es una variable que no se usa. Entonces, se preguntará ¿para que está?. Bien, se utiliza para diferenciar del constructor anterior (ambos usan 4 parámetros float). Nunca pueden existir dos constructores de la misma clase que reciban los mismos tipos de parámetros.
- new Line(Vector2f start, Vector2f end): crea una nueva línea en base a dos vectores. El primer vector indica el punto inicial. El segundo vector indica el punto final.
La clase Vector2f es una clase que también se encuentra en el paquete org.newdawn.slick.geom cuyo objetivo es representar un vector de dos dimensiones, es decir, 2 coordenadas.
Para construir uno, haga simplemente:
Vector2f v = new Vector2f(30, 30);
Donde este vector representa la coordenada (30, 30).
Más adelante estudiaremos la clase Vector2f en detalle.
Tener en cuentra que la clase Line se debe dibujar y no pintar. Es decir, debemos usar el método draw() y no el método fill() de la clase Graphics.
Métodos.
La clase Line provee muchos métodos. A continuación se listan los más importantes. Si desea verlos a todos puede consultar el javadoc.
- float distance(Vector2f punto): devuelve la menor distancia de un punto a la línea.
- float distanceSquared(Vector2f punto): devuelve la menor distancia de un punto a la línea pero elevado al cuadrado. Si se pregunta ¿para qué existe este método?, la respuesta es para mayor eficiencia y se podría usar en comparaciones. Usted me podría decir, ¿dónde está la mayor eficiencia si estoy elevando al cuadrado al resultado? Pues, resulta que no es así. El resultado en sí es al cuadrado y el qué hace el cálculo para obtener la distancia es el método distance() calculando la raíz cuadrada de ese valor.
Vea la implementación del método distance():
public float distance(Vector2f point) {
return (float) Math.sqrt(distanceSquared(point));
}
Note que es el método distanceSquared() él que calcula la distancia al cuadrado y el método distance() calcula su raíz cuadrada.
- void getClosestPoint(Vector2f point, Vector2f result): devuelve el punto contenido en la línea más cercano a un punto dado. El primer parámetro es el punto por el cual tenemos que calcular al más cercano en la línea y el segundo es dónde se va a almacenar nuestro resultado. A continuación un ejemplo:
Line linea = new Line(200, 200);
Vector2f v = new Vector2f(50, 41);
Vector2f vResult = new Vector2f();
linea1.getClosestPoint(v, vResult);
System.out.println("(" + vResult.getX() + ", " + vResult.getY() + ")");
Salida:
(45.5, 45.5)
Es decir, el punto (45.5, 45.5) es un punto contenido en la línea y a su vez es el más cercano al punto (50, 41).
- Vector2f getStart(): devuelve un vector con el punto inicial de la recta.
- Vector2f getEnd(): devuelve un vector con el punto final de la recta.
- Vector2f intersect(Line otra): devuelve el punto de intersección entre esta línea y otra pasada como parámetro. Si no existe intersección entonces devuelve null. Tener en cuenta que a este método no le importa el segmento de recta visible, esto es como si se extendieran ambas rectas hasta el infinito y a partir de allí se busca el punto de intersección. De acuerdo a esto podemos concluir que este método sólo devolveránull en caso que las rectas sean paralelas no coincidentes. Si las rectas son paralelas coincidentes entonces también devolverá null ya que se puede pensar que son infinitos los puntos de intersección. En definitiva, sólo devolverá null cuando las rectas sean paralelas.
- Vector2f intersect(Line otra, boolean limitado): devuelve el punto de intersección de esta línea con otra pasada como parámetro. Si el segundo parámetro vale true entonces el método tiene en cuenta el segmento de recta visible, es decir, no hace la "extensión" de la recta. Si el segundo parámetro vale false entonces el método se comporta igual que el anterior.
- boolean intersects(Shape shape): devuelve true si la línea interseca con la figura pasada como parámetro.
- boolean on(Vector2f punto): devuelve true si el punto pasado como parámetro está contenido en la línea. No tiene en cuenta la extensión de la línea.
- void set(Vector2f puntoInicial, Vector2f puntoFinal): cambia la configuración de la recta mediante el punto inicial y el punto final.Terminamos con los métodos y ahora para no aburrir (espero) veremos una aplicación de dibujar líneas rectas. Muchos pueden creer que con sólo dibujar líneas rectas no se puede hacer mucho o quizá se necesite mucho esfuerzo pero veremos que con la ayuda de la recursividad podemos diseñar figuras realmente complejas.Fractales.Antes de comenzar a hablar de fractales, es necesario aclarar que esta sección no forma parte de Slick2D, sólo usaremos Slick2D para dibujar nuestros fractales. Usted puede obviar esta sección sin problemas.Un fractal es una figura geométrica formada por patrones que se repiten muchas veces y generan figuras bastantes complejas. Si bien sólo acá voy a mostrar un sólo ejemplo del uso de fractales pero la aplicación de esto no tiene límites, sólo su creatividad. Tampoco voy a explicar la fundamentación matemática aunque sólo es un poco de geometría elemental.Existen muchos ejemplos de fractales a los que se les han puesto un nombre para mayor comodidad, tal como, el copo de nieve de Von Koch, que se muestra en la siguiente figura.Observe que la figura anterior está hecha puramente con líneas rectas. Lo que haremos en este pequeño ejemplo es crear justamente un copo de nieve de Von Koch.Lo primero que necesitamos hacer es crear una curva de Von Koch, que se puede apreciar en la siguiente figura:Observe que la curva se parece a la parte de abajo del copo de nieve. Lo que hay que hacer es repetir esta curva de tal manera que se forme el copo. Pero antes, la pregunta es, ¿Cómo hacemos esa curva?. Bien, recuerde que habíamos hablado que detrás de todo esto está la recursión, veamos de qué hablaba. Comenzamos con una línea recta...En este punto no hay recursividad, lo vamos a llamar, recursividad cero. Ahora, partimos a esa línea en 4 partes y formamos la siguiente figura:
Observe que la línea recta la "dividimos" en 4 partes creando una desviación en la curva que antes era recta. Observe, también, que de una recta obtuvimos cuatro rectas distintas. En este momento estamos en la recusión uno. Pero imagínese que este proceso continúa así dos o tres veces más, es decir, tomamos la parte uno y la dividimos en 4 desviándola, tomamos la parte 2 y también la dividimos en 4 desviándola, con las partes 3 y 4 hacemos lo mismo. La ilustración 8 muestra una curva con recursividad 2.¿Nota que cada parte se dividió en 4 partes? Eso se hace con la recursión.A continuación se brinda el código que arma un copo de nieve de Von Koch. Recuerde que la intención en este momento no es explicar Fractales sino simplemente ver una aplicación de dibujo con líneas rectas y que si no sabía que era un fractal, pues ahora lo sabe.import java.util.ArrayList;import org.newdawn.slick.AppGameContainer;import org.newdawn.slick.BasicGame;import org.newdawn.slick.Color;import org.newdawn.slick.GameContainer;import org.newdawn.slick.Graphics;import org.newdawn.slick.SlickException;import org.newdawn.slick.geom.Line;public class MiJuego extends BasicGame {private AppGameContainer contenedor;private ArrayList<Line> lineas;private final float DESPLAZAMIENTO = 200;public MiJuego() throws SlickException {super("Copos de nieve de Von Koch");contenedor = new AppGameContainer(this);contenedor.setShowFPS(false);contenedor.setDisplayMode(500, 500, false);contenedor.start();}@Overridepublic void init(GameContainer container) throws SlickException {contenedor.getGraphics().setBackground(Color.white);lineas = new ArrayList<>();copoVonKoch(360, 3);}public void curvaVonKoch(float xi, float yi, float xf, float yf, int n) {if (n == 0) {Line line = new Line(xi + DESPLAZAMIENTO, yi, xf + DESPLAZAMIENTO, yf);lineas.add(line);} else {float xc = xi + (xf - xi) / 3.0f;float yc = yi + (yf - yi) / 3.0f;float xd = xf - (xf - xi) / 3.0f;float yd = yf - (yf - yi) / 3.0f;float xe = (float) ((xc + xd) * Math.cos(Math.PI / 3.0) - (yd - yc) * Math.sin(Math.PI / 3.0));float ye = (float) ((yc + yd) * Math.cos(Math.PI / 3.0) + (xd - xc) * Math.sin(Math.PI / 3.0));curvaVonKoch(xi, yi, xc, yc, n - 1);curvaVonKoch(xc, yc, xe, ye, n - 1);curvaVonKoch(xe, ye, xd, yd, n - 1);curvaVonKoch(xd, yd, xf, yf, n - 1);}}public void copoVonKoch(float p, int n) {float x1 = 0;float y1 = 0;float x2 = (float) (p * Math.cos(2 * Math.PI / 3.0));float y2 = (float) (p * Math.sin(2 * Math.PI / 3.0));float x3 = (float) (p * Math.cos(Math.PI / 3.0));float y3 = (float) (p * Math.sin(Math.PI / 3.0));curvaVonKoch(x1, y1, x2, y2, n);curvaVonKoch(x2, y2, x3, y3, n);curvaVonKoch(x3, y3, x1, y1, n);}@Overridepublic void update(GameContainer container, int delta) throws SlickException {}@Overridepublic void render(GameContainer container, Graphics g) throws SlickException {g.setColor(Color.black);for (Line l : lineas) {g.draw(l);}}}Si no entiende de dónde salieron tantos cálculos, no se preocupe, no es que algo que tenga que saber para programar con Slick2D, aunque nada está demás. Más adelante, veremos muchos fractales y en detalle.
No hay comentarios:
Publicar un comentario