7.3 Volviendo a los Morphs de Spacewar!

7.3.1 Estrella central

Nuestra estrella central tiene una extensión de 30 @ 30 que necesitamos utilizar en varios lugares del código. Por lo tanto, tiene sentido definir un método específico para responder a este valor:

CentralStar>>morphExtent
   ^ `30 @ 30`

Ejemplo 7.3: Extensión de la estrella central

 note Una expresión rodeada por comillas invertidas '`' solo se evalúa una vez, cuando el método se guarda y compila por primera vez. Esto crea un valor literal compuesto y mejora el rendimiento del método, ya que la expresión no se evalúa cada vez que se llama al método: en su lugar, se utiliza el valor precompilado.

Como aprendiste anteriormente, un morph se dibuja a sí mismo desde su método drawOn:. Dibujamos la estrella como una elipse con radios x e y que fluctúan aleatoriamente:

CentralStar>>drawOn: canvas
   | radius |
   radius := self morphExtent // 2.
   canvas ellipseCenter: 0 @ 0
      radius: (radius x + (2 atRandom - 1)) @ (radius y + (2 atRandom - 1))
      borderWidth: 3 
      borderColor: Color orange 
      fillColor: Color yellow

Ejemplo 7.4: Una estrella con un tamaño fluctuante

Los diámetros de la estrella en las direcciones x e y fluctúan independientemente entre 0 y 2 unidades. La estrella no parece perfectamente redonda.

ch07-09-fluctuatingStar

Figura 7.9: Una estrella con un tamaño fluctuante

7.3.3 Torpedo

Al igual que una nave espacial, cuando se instancia un torpedo, su proa apunta hacia la parte superior de la pantalla y sus vértices vienen dados por la Figura 7.11.

ch07-11-TorpedoDiagram

Figura 7.11: Diagrama de torpedo al inicio del juego

 CuisLogo Dados los vértices proporcionados por la Figura 7.11, ¿cómo escribirías su método morphExtent?

Ejercicio 7.5: Extensión del torpedo

 CuisLogo ¿Cómo escribirás el método drawOn: de Torpedo?

Ejercicio 7.6: Dibujo de torpedo

7.3.4 Revisar el dibujado

Como habrás observado, los métodos drawOn: de SpaceShip y Torpedo comparten la misma lógica: dibujar un polígono a partir de sus vértices. Probablemente queramos trasladar esta lógica común a su antecesor común, la clase Mobile. Necesita conocer sus vértices, por lo que quizá queramos añadir una variable de instancia vertices inicializada en sus subclases con una matriz que contenga los puntos:

PlacedMorph subclass: #Mobile
   instanceVariableNames: 'acceleration color velocity vertices'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'Spacewar!'

SpaceShip>>initialize
   super initialize.
   vertices := {0@-15 . -10@15 . 0@10 . 10@15}.
   self resupply
   
Torpedo>>initialize
   super initialize.
   vertices := {0@-4 . -2@4 . 2@4}.
   lifeSpan := 500.
   acceleration := 3000

Sin embargo, esto no es una buena idea. Imagina el juego con 200 torpedos: ¡la matriz de vértices se duplicaría 200 veces con los mismos datos!

Variable de clase

En ese tipo de situación, lo que se necesita es una variable de clase definida en el lado de la clase, en contraste con el lado de la instancia, donde hemos estado programando hasta ahora.

Aprovechamos el hecho de que todos los objetos son instancias de alguna clase. ¡La Mobile class es una instancia de la clase Class!

  1. Una variable de clase sólo puede ser accedida desde la propia clase en un método de clase.
  2. Una entidad (por ejemplo, un torpedo disparado) puede acceder a las variables de clase a través de métodos de clase, enviando un mensaje a una clase (por ejemplo, Torpedo) en lugar de a sí misma o a otra entidad.
  3. En la jerarquía de clases, cada subclase tiene su propia instancia de la variable de instancia de clase y puede asignarle un valor diferente, a diferencia de una variable de clase, que se comparte entre todas las subclases (se explica más adelante).
  4. Para editar las variables de clase y los métodos de clase, en el Browser del sistema, pulsa el botón class situado debajo de la lista de clases.

En el navegador del sistema, hacemos clic en el botón class y luego declaramos nuestra variable en la definición de la Mobile classFigura 7.12:

Mobile class
   instanceVariableNames: 'vertices'

Ejemplo 7.6: vertices como variable instanciada en la Mobile class

A continuación, escribimos un método de acceso en Mobile class, para que las instancias SpaceShip y Torpedo puedan acceder a él:

Mobile class>>vertices
   ^ vertices
ch07-12-browserClassSide

Figura 7.12: El lado de clase del Browser del sistema

A continuación, cada subclase es responsable de inicializar correctamente vertices con su método de clase initialize:

SpaceShip class>>initialize
"SpaceShip initialize"
   vertices :=  {0@-15 . -10@15 . 0@10 . 10@15}

Torpedo class>>initialize
"Torpedo initialize"
   vertices := {0@-4 . -2@4 . 2@4}

Ejemplo 7.7: Inicializar una clase

Cuando se instala una clase en Cuis-Smalltalk, se ejecuta su método de clase initialize. También puedes seleccionar el método y ejecutalo con Ctrl-d.

Experimenta en un espacio de trabajo para comprender cómo se comporta una variable de instancia de clase:

SpaceShip vertices.
⇒ nil 
SpaceShip initialize.
SpaceShip vertices.
⇒ #(0@-15 -10@15 0@10 10@15) 

Torpedo vertices.
⇒ nil 
Torpedo initialize.
Torpedo vertices.
⇒ #(0@-4 -2@4 2@4)

Ejemplo 7.8: El valor de una variable de clase no es compartido por las subclases

Este es realmente el comportamiento que queremos: las instancias SpaceShip y Torpedo tienen un diagrama diferente. Sin embargo, todas las instancias de una SpaceShip tendrán el mismo diagrama, haciendo referencia a la misma matriz vertices (es decir, la misma ubicación en la memoria del ordenador).

Cada instancia pregunta a su lado de clase con el mensaje #class:

aTorpedo class
⇒ Torpedo
self class
⇒ SpaceShip

El método drawOn: de Torpedo se reescribe para acceder a los vértices en su lado de clase:

Torpedo>>drawOn: canvas
   | vertices |
   vertices := self class vertices.
   canvas line: vertices first to: vertices second width: 2 color: color.
   canvas line: vertices third to: vertices second width: 2 color: color.
   canvas line: vertices first to: vertices third width: 2 color: color

 CuisLogo ¿Cómo reescribirías drawOn: de SpaceShip para utilizar los vértices de su lado de clase?

Ejercicio 7.7: Acceso de la nave espacial a su diagrama en el lado de la clase

Hasta ahora, seguimos teniendo esta redundancia en los métodos drawOn:. Lo que queremos es que Mobile se encargue de dibujar el polígono dada una matriz de vértices: self drawOn: canvas polygon: vertices.

El método drawOn: de SpaceShip y Torpedo se escribirá simplemente como:

Torpedo>>drawOn: canvas
   self drawOn: canvas polygon: self class vertices

SpaceShip>>drawOn: canvas
   | vertices |
   vertices := self class vertices.
   self drawOn: canvas polygon: vertices.
   "Draw gas exhaust"
   acceleration ifNotZero: [
      canvas line: vertices third to: 0@35 width: 1 color: Color gray]

 CuisLogo ¿Cómo escribirías el método drawOn:polygon: en Mobile? Sugerencia: utiliza el iterador withIndexDo:.

Ejercicio 7.8: Dibujar en Mobile

Variable de clase

Una variable de clase se escribe en mayúsculas en el argumento de la palabra clave classVariableNames::

PlacedMorph subclass: #Mobile
   instanceVariableNames: 'acceleration color velocity'
   classVariableNames: 'Vertices'
   poolDictionaries: ''
   category: 'Spacewar!'

Ejemplo 7.9: Vertices una variable de clase Mobile

Como variable de instancia de clase, se puede acceder a ella directamente desde el lado de la clase y las instancias solo tienen acceso mediante mensajes enviados al lado de la clase. A diferencia de una variable de instancia, su valor es común en toda la jerarquía de clases.

En Spacewar!, una variable de clase Vertices tendrá el mismo diagrama para una nave espacial y un torpedo. Esto no es lo que queremos.

SpaceShip>>vertices
   ^ `{0@-15 . -10@15 . 0@10 . 10@15}`

7.3.5 Dibujar simplificado

Usar una variable de clase en el diseño actual del juego es un poco excesivo. Fue una excusa para presentar el concepto de variables de clase. Si el juego incluyera un editor en el que el usuario rediseñara los diagramas de la nave y los torpedos, tendría sentido guardar los vértices en una variable. Pero nuestros vértices de los diagramas de la nave espacial y los torpedos son constantes. No los modificamos. Al igual que hicimos con la masa de la nave espacial –Ejemplo 3.16–, podemos utilizar un método que devuelva una colección, rodeada de llaves para mejorar la eficiencia.

SpaceShip>>vertices
   ^ `{0@-15 . -10@15 . 0@10 . 10@15}`
   
Torpedo>>vertices
   ^ `{0@-4 . -2@4 . 2@4}`

Ejemplo 7.10: Vértices devueltos por un método de instancia

Luego, en los métodos de dibujo, reemplazamos self class vertices por self vertices.

7.3.6 Revisar colisiones

En Ejemplo 4.23, tenemos un enfoque muy ingenuo para la colisión entre la estrella central y las naves, basado en la distancia entre los morphs. Era muy impreciso.

En los juegos de bitmaps, una forma clásica de detectar colisiones es observar las intersecciones de los rectángulos que rodean los objetos gráficos. El mensaje #displayBounds enviado a un morph responde a sus límites en la pantalla, un rectángulo que abarca el morph dada su rotación y escala.

ch07-13-shipDisplayBounds

Figura 7.13: Los límites de visualización de una nave espacial

Al examinar la clase Rectangle, se aprende que el mensaje #intersects: nos indica si dos rectángulos se superponen. Esto es lo que necesitamos para una detección de colisiones más precisa entre la estrella central y las naves espaciales:

SpaceWar>>collisionsShipsStar
   ships do: [:aShip | 
      (aShip displayBounds intersects: centralStar displayBounds) ifTrue: [
         aShip flashWith: Color red.
         self teleport: aShip]]

Ejemplo 7.11: Colisión (superposición de rectángulos) entre las naves y el Sol.

Sin embargo, utilizamos el framework VectorGraphics, que conoce la forma de cada morph que está renderizando. Utiliza este conocimiento para la detección de colisiones con precisión de píxeles. Reescribimos nuestro método de detección de forma más sencilla con el mensaje #collides::

SpaceWar>>collisionsShipsStar
   ships do: [:aShip | 
      (aShip collides: centralStar) ifTrue: [
         aShip flashWith: Color red.
         self teleport: aShip]]

Ejemplo 7.12: Colisión (precisión de píxel) entre las naves y el Sol

 CuisLogo Reescribe los tres métodos de detección de colisiones entre naves espaciales, torpedos y la estrella central.

Ejercicio 7.9: Detección precisa de colisiones