Durante l’ultima esercitazione di laboratorio abbiamo visto che ci sono dei limiti alle figure geometriche che java fornisce, e che occorre quindi ricorrere alla matematica per poter sviluppare geometrie più complesse.
Un esempio lampante è stato il quadrato creato al centro della finestra con la classe Rectangle2D.

E ci eravamo posti il problema: Se volessimo ruotare questo quadrato di 45° come si potrebbe procedere?.
Apparentemente non ci sono soluzioni. Non si possono manipolare le figure geometriche standard con metodi delle classi geometriche di appartenenza.
Come visto durante le lezioni di Metodi Numerici per la Grafica, esistono delle applicazioni lineari che sono in grado di ruotare i punti nello spazio mantenendo fisse le distanze di questi punti dall’origine.
Queste applicazioni lineari sono, nello specifico, delle matrici ortogonali. Le applicazioni lineari si chiamano Trasformazioni Affini e permettono, sfruttando la classe java AffineTransform di modificare le geometrie degli oggetti grafici rispettando le caratteristiche di queste applicazioni lineari.
Difatti è possibile ruotare una figura geometrica e traslarla, rispetto l’origine dello spazio in cui è immersa.
Per semplificare la costruzione, e quindi passare al codice solamente un elemento (una matrice) e non due (una matrice di rotazione più un vettore di traslazione), la classe che gestisce le trasformazioni affini ammette le trasformazioni con matrici omogenee. Questo implica che invece di avere una cosa di questo tipo:
$$ \begin{pmatrix}x’\\y’\end{pmatrix} = \begin{pmatrix} \cos{\theta} & \sin{\theta}\\ -\sin{\theta} & \cos{\theta} \end{pmatrix} \begin{pmatrix} x\\y \end{pmatrix} + \begin{pmatrix}t_x\\t_y\end{pmatrix} = \begin{pmatrix} x\cos{\theta}+y\sin{\theta}+t_x \\ -x\sin{\theta}+y\cos{\theta}+t_y \end{pmatrix}$$
l’applicazione diventa:
$$ \begin{pmatrix} x’\\y’\\1 \end{pmatrix} = \begin{pmatrix} \cos{\theta} & \sin{\theta} & t_x\\ -\sin{\theta} & \cos{\theta} & t_y \\ 0 & 0 & 1 \end{pmatrix}\begin{pmatrix} x\\y\\1 \end{pmatrix} = \begin{pmatrix} x\cos{\theta}+y\sin{\theta}+t_x \\ -x\sin{\theta}+y\cos{\theta}+t_y \\ 1 \end{pmatrix}$$
Idealmente quindi si ha una rotazione di angolo $$\theta$$ intorno all’asse normale allo schermo.
Con questi concetti, quindi, applicati al quadrato iniziale, si può fare una rotazione di 45° ottenendo:

Ora.. questi concetti possono essere applicati in modo non sterile per realizzare per lo più piccoli giochi, come il vecchissimo asteroids.
In laboratorio avevamo iniziato a svilupparne una versione, ma per mancanza di tempo non abbiamo potuto nemmeno iniziare ad applicare le trasformazioni affini.
Quella che propongo qui di seguito è una versione di asteroids abbastanza completa.
Purtroppo ci sono alcuni bug con il refresh degli asteroidi, e l’astronave non esplode quando collide con un asteroide, ma queste sono modifiche che potete apportare voi al codice.
Per quello che mi riguarda ho cercato di sviluppare il gioco sfruttando un approccio algoritmico.
Gli asteroidi, ad esempio, sono generati randomicamente anche nella forma. E si spotano rimbalzando sui bordi della finestra finché un proiettile della navetta non li colpisce, distruggendoli.
Questo è uno screen del gioco in funzione:

Qui metto anche il codice. Dategli un’occhiatina e cercate di capire come funzionano le trasformazioni affini in java.
In questo esempio non sono state usate matrici omogenee, ma metodi della classe AffineTransform (ma che sostanzialmente generano matrici).
Durante le prossime lezioni di laboratorio vedremo meglio come applicare una trasformazione usando le matrici omogenee, perché oggetti più comodi sia da manipolare, sia da utilizzare.
1 2 3 4 5 6 7 8 9 10 11 12 13 | import javax.swing.*; public class JAsteroids { public static void main(String[] argv) { JFrame f = new JFrame("A S T E R O I D S"); f.getContentPane().add(new AsteroidPane()); f.setSize(500, 500); f.setDefaultCloseOperation(3); f.setVisible(true); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import java.util.*; import javax.swing.JPanel; class AsteroidPane extends JPanel { private float m; private Navicella nav = null; private ArrayList asteroids = null; private ArrayList bullets = null; public AsteroidPane() { super(); setFocusable(true); requestFocus(); nav = new Navicella(new Point(250, 250)); asteroids = new ArrayList(); bullets = new ArrayList(); nav.scala(1.5f); eventHandler(); } public void eventHandler() { addMouseMotionListener(new MouseMotionAdapter() { public void mouseMoved(MouseEvent e) { Point2D p = nav.getPoint(); m = (float)((e.getY()-p.getY())/(e.getX()-p.getX())); m = (float)(Math.atan(m) - Math.PI/2); m = (e.getX() < p.getX())? m : m+(float)Math.PI ; nav.ruota(m); } }); addMouseListener(new MouseAdapter() { public void mousePressed(MouseEvent e) { bullets.add(new Bullet(nav.getPoint(), m-(float)Math.PI/2)); } }); addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { switch(e.getKeyCode()) { case KeyEvent.VK_UP: nav.sposta(new Point2D.Double(nav.getPoint().getX(), nav.getPoint().getY()-5)); break; case KeyEvent.VK_DOWN: nav.sposta(new Point2D.Double(nav.getPoint().getX(), nav.getPoint().getY()+5)); break; case KeyEvent.VK_LEFT: nav.sposta(new Point2D.Double(nav.getPoint().getX()-5, nav.getPoint().getY())); break; case KeyEvent.VK_RIGHT: nav.sposta(new Point2D.Double(nav.getPoint().getX()+5, nav.getPoint().getY())); break; } } }); new Thread(new Runnable() { public void run() { while(true) { if (asteroids.size() < 3) { double rnd = Math.random() * (2*Math.PI); try { Thread.sleep((int)(Math.random()*1+500)); int x = (int)(Math.cos(rnd)*getWidth()/2+getWidth()/2); int y = (int)(Math.sin(rnd)*getHeight()/2+getHeight()/2); Asteroid app = new Asteroid(new Point2D.Float(x, y)); app.setBounds(new Dimension(getWidth(), getHeight())); asteroids.add(app); } catch (Exception e) {} } } } }).start(); new Thread(new Runnable() { public void run() { while (true) { int[] rm = {-1, -1}; repaint(); for (int i=0; i<bullets.size(); i++) { Bullet app = (Bullet)bullets.get(i); for (int j=0; j<asteroids.size(); j++) { if (app.contains( ((Asteroid)asteroids.get(j)).getShape() )) { rm[0] = i; rm[1] = j; System.out.println(i + " - " + j); } } } if (rm[1] != -1) asteroids.remove(rm[1]); if (rm[0] != -1) bullets.remove(rm[0]); } } }).start(); } public void paintComponent(Graphics g) { g.setColor(Color.BLACK); g.fillRect(0, 0, getWidth(), getHeight()); } public void paint(Graphics _g) { Graphics2D g = (Graphics2D)_g; g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); paintComponent(g); nav.fill(g); for (int i=0; i<asteroids.size(); i++) ((Asteroid)asteroids.get(i)).fill(g); for (int i=0; i<bullets.size(); i++) g.fill( (Ellipse2D)bullets.get(i) ); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | import java.awt.*; import java.awt.geom.*; abstract class Sprite implements Runnable { protected AffineTransform af = null; private Point2D p = null; private float theta = 0; private float s = 1; public Thread t = null; public Sprite(Point2D p) { start(); af = new AffineTransform(); sposta(p); } public void sposta(Point2D p) { this.p = p; } public void ruota(float theta) { this.theta = theta; } public void scala(float s) { this.s = s; } public Shape affine(Shape scp) { af.setToIdentity(); af.translate(p.getX(), p.getY()); af.rotate(theta); af.scale(s, s); return af.createTransformedShape(scp); } public Point2D getPoint() { return p; } public void start() { if (t == null) { t = new Thread(this); t.start(); } } public void stop() { t = null; } public abstract void run(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import java.awt.*; import java.awt.geom.*; class Navicella extends Sprite { private Polygon pol = null; private Shape s = null; public Navicella(Point2D p) { super(p); pol = new Polygon(); af = new AffineTransform(); polNav(); } public void polNav() { pol.addPoint(0, -30); pol.addPoint(-10, +5); pol.addPoint(+10, +5); } public void run() {} public void fill(Graphics2D g) { g.setColor(new Color(1f, 1f, 1f)); g.draw(affine(pol)); g.setColor(new Color(1, 1, 1, .1f)); g.fill(affine(pol)); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | import java.awt.*; import java.awt.geom.*; class Asteroid extends Sprite { private Polygon pol = null; private float vx; private float vy; private float x; private float y; private float incx; private float incy; private Dimension dim = null; private Shape s = null; public Asteroid(Point2D p) { super(p); start(); pol = new Polygon(); vx = (float)( Math.cos( Math.random() * (2 * Math.PI) ) ); vy = (float)( Math.sin( Math.random() * (2 * Math.PI) ) ); incx = (float)Math.random() * 3 + 1; incy = (float)Math.random() * 3 + 1; sposta(p); genAsteroid(); } public void setBounds(Dimension dim) { this.dim = dim; } public void run() { while(t != null) { if (dim != null) { if (x < 0 || x > dim.getWidth()) vx *= -1; if (y < 0 || y > dim.getHeight()) vy *= -1; } x+=vx*incx; y+=vy*incy; sposta(new Point2D.Float(x, y)); try { Thread.sleep(30); } catch (Exception e) {} s = affine(pol); } } public void genAsteroid() { float scost = (float)(Math.random() * Math.PI); for (float i=0; i<2*Math.PI; i+=3*Math.PI/7) { float maxr = (float)Math.random() * 40; float minr = (float)Math.random() * 20; float r = (Math.random() + Math.random() < Math.random()-Math.random()) ? maxr : minr; pol.addPoint((int)((r+30) * Math.cos(i+scost)), (int)((r+30) * Math.sin(i+scost))); } } public void fill(Graphics2D g) { if (s != null) { g.setColor(Color.WHITE); g.draw(s); g.setColor(new Color(1, 1, 1, .3f)); g.fill(s); } } public Shape getShape() { return affine(pol); } public boolean contains(Shape scp) { return s.contains(scp.getBounds2D()); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | import java.awt.Shape; import java.awt.geom.*; class Bullet extends Ellipse2D.Double implements Runnable { private float theta = 0; private float dist = 0; private static float DIM = 10; private Thread t = null; private Point2D p; private Shape s = null; public Bullet(Point2D p, float theta) { super(p.getX()-DIM/2, p.getY()-DIM/2, DIM, DIM); this.p = p; this.theta = theta; start(); } public void run() { while(t != null) { dist++; x = p.getX() + dist * Math.cos(theta) - DIM/2; y = p.getY() + dist * Math.sin(theta) - DIM/2; try { Thread.sleep(5); } catch (Exception e) {} } } public void start() { if (t == null) { t = new Thread(this); t.start(); } } public float getDist() { return dist; } public boolean contains(Shape scp) { return this.intersects(scp.getBounds2D()); } } |
