Anteriormente definimos a los actores del juego como subclases de la
clase muy general Object (véase
Ejemplo 3.14). Sin embargo, el juego, la estrella
central, las naves y los torpedos son objetos visuales, cada uno con una
forma gráfica específica:
Por lo tanto, tiene sentido convertir estos actores en tipos de Morphs, la entidad visual de Cuis-Smalltalk. Para ello, dirija un
navegador del sistema a la definición de clase de cada actor, sustituya
la clase padre Object por PlacedMorph21, y luego guarde la definición
de clase con Ctrl-s.
Por ejemplo, la clase torpedo tal y como se ve en Ejemplo 3.14 se modifica como:
PlacedMorph subclass: #Torpedo instanceVariableNames: 'position heading velocity lifeSpan' classVariableNames: '' poolDictionaries: '' category: 'Spacewar!'
Además, un placed Morph ya conoce su posición y orientación en la
pantalla: se puede arrastrar y mover en la pantalla y girar con el
cursor del ratón. Por lo tanto, las variables de instancia
position y heading son redundantes y deben
eliminarse. Por ahora las mantenemos, pero se eliminarán más adelante,
cuando sepamos cómo sustituir cada uno de sus casos de uso por su
equivalente Morph adecuado.
Modifica
SpaceWar,CentralStarySpaceShippara que sean subclases de la clasePlacedMorph.
Ejercicio 6.1: Hacer todos los Morphs
Como se ha explicado en las secciones anteriores de este capítulo, un
morph puede estar incrustado dentro de otro morph. En Spacewar!, una
instancia morph SpaceWar que presenta el juego, es la
propietaria de los morphs de la estrella central, la nave espacial y
los torpedos. En otras palabras, la estrella central, las naves
espaciales y los torpedos son submorphs de una instancia morph
SpaceWar.
El código SpaceWar>>initializeActors en Ejemplo 4.17 no
está completo sin añadir y posicionar la estrella central y las naves
espaciales como submorfos del juego Spacewar!:
SpaceWar>>initializeActors
centralStar := CentralStar new.
self addMorph: centralStar.
centralStar morphPosition: 0 @ 0.
torpedoes := OrderedCollection new.
ships := Array with: SpaceShip new with: SpaceShip new.
self addAllMorphs: ships.
ships first
morphPosition: 200 @ -200;
color: Color green.
ships second
morphPosition: -200 @ 200;
color: Color red
Ejemplo 6.3: Completar el código para inicializar los actores de Spacewar!
Hay dos mensajes importantes: #addMorph: y #morphPosition:. El
primero le pide al receptor morph que incruste su argumento morph como
un submorph, el segundo le pide que establezca las coordenadas del
receptor en el marco de referencia de su propietario. Al leer el código,
se deduce que el origen del marco de referencia del propietario es su
centro, de hecho, nuestra estrella central está en el centro del juego.
Hay un tercer mensaje que no aparece aquí, #morphPosition, para
solicitar las coordenadas del receptor en el marco de referencia de su
propietario.
Recuerda nuestra discusión sobre la variable de instancia
position. Ahora entiendes claramente que es redundante y la
eliminamos de las definiciones SpaceShip y Torpedo. Cada
vez que necesitamos acceder a la posición, simplemente escribimos
self morphPosition y cada vez que necesitamos modificar la
posición escribimos self morphPosition: newPosition. Más
adelante hablaremos más sobre esto.
En nuestro modelo newtoniano explicamos que las naves espaciales están sujetas a la aceleración del motor y a la fuerza gravitatoria de la estrella central. Las ecuaciones se describen en la Figura 2.4.
Basándonos en estas matemáticas, escribimos el método
SpaceShip>>update: para actualizar la posición de la nave
según el tiempo transcurrido; véase Ejemplo 4.19.
Hasta ahora, en nuestro modelo, un torpedo no está sujeto a la fuerza
gravitatoria de la estrella central ni a la aceleración de su motor. Se
supone que su masa es cero, lo cual es poco probable. Por supuesto, el
método Torpedo>>update: es más sencillo que el de la nave espacial
(véase Ejemplo 4.18. Sin embargo, es más
preciso y aún más divertido que los torpedos estén sujetos a la fuerza
gravitatoria22 y
a la aceleración de su motor; un piloto de nave espacial hábil podría
utilizar la asistencia gravitatoria para acelerar un torpedo disparado
con una trayectoria cercana a la estrella central.
¿Qué repercusiones tienen estas consideraciones en las entidades torpedo y nave espacial?
Los estados y comportamientos compartidos sugieren una clase común. Los
estados y comportamientos no compartidos sugieren subclases
especializadas que incorporan las diferencias. Así que «extraigamos» los
elementos compartidos de las clases SpaceShip y Torpedo
en una clase ancestral común, una más especializada que la clase
Morph que comparten actualmente.
Realizar este análisis en el modelo informático del juego forma parte del esfuerzo de refactorización para evitar duplicaciones de comportamiento y estado, al tiempo que se hace más evidente la lógica común en las entidades. La idea general de la refactorización del código es reelaborar el código existente para hacerlo más elegante, comprensible y lógico.
Para ello, introduciremos una clase Mobile, un tipo de
PlacedMorph con comportamientos específicos de un objeto
móvil sometido a aceleraciones. Sus estados son la masa, la posición, el
rumbo, la velocidad y la aceleración. Bueno, ya que estamos hablando de
refactorización, el estado de masa no tiene mucho sentido en nuestro
juego, ya que la masa de nuestro móvil es constante. Solo necesitamos un
método que devuelva un número literal y entonces podremos eliminar la
variable de instancia mass. Además, como se ha explicado
anteriormente, una instancia PlacedMorph ya conoce su
posición y rumbo, por lo que también eliminamos estos dos atributos,
aunque hay comportamientos comunes a una nave espacial y un torpedo.
El resultado es esta definición de Mobile:
PlacedMorph subclass: #Mobile instanceVariableNames: 'velocity acceleration color' classVariableNames: '' poolDictionaries: '' category: 'Spacewar!'
Ejemplo 6.4: Mobile en el juego
¿Cuáles deberían ser las definiciones refactorizadas de las clases
SpaceShipyTorpedo?
Ejercicio 6.2: Refactorizar SpaceShip y
Torpedo
Los primeros comportamientos que añadimos a nuestro Mobile son
su inicialización y su masa:
Mobile>>initialize
super initialize.
velocity := 0 @ 0.
acceleration := 0
Mobile>>mass
^ 1
Los siguientes métodos que se añadirán son los relativos a los cálculos físicos. En primer lugar, el código para calcular la aceleración gravitatoria:
Mobile>>gravity "Compute the gravity acceleration vector" | position | position := self morphPosition. ^ -10 * self mass * owner starMass / (position r raisedTo: 3) * position
Ejemplo 6.5: Calcular la fuerza de la gravedad
Este método merece algunos comentarios:
self morphPosition devuelve una instancia @.class
Point@, la posición del móvil en el marco de referencia del
propietario (owner).
owner es la instancia SpaceWar que
representa el juego. Es el propietario (morph padre) del móvil. Cuando
se le pregunta #starMass, consulta la masa de su estrella
central y devuelve su valor:
SpaceWar>>starMass ^ centralStar mass
Como beneficio adicional, podemos eliminar el método starMass
definido anteriormente en la clase SpaceShip.
position r, El mensaje #r solicita el
atributo radio de un punto considerado en coordenadas polares. Es
simplemente su longitud. Es la distancia entre el móvil y la estrella
central.
* position realmente significa multiplicar el valor
escalar anterior por un Point, es decir, un vector. Por lo
tanto, el valor devuelto es un Point, un vector en este
contexto, el vector de gravedad.
El método para actualizar la posición y la velocidad del móvil es
prácticamente el mismo que en Ejemplo 4.19. Por supuesto, las
versiones SpaceShip>>update: y Torpedo>>update:
deben eliminarse ambas. A continuación se muestra la versión completa
con la forma en que morph accede a la posición del móvil:
Mobile>>update: t
"Update the mobile position and velocity"
| ai ag newVelocity |
"acceleration vectors"
ai := acceleration * self direction.
ag := self gravity.
newVelocity := (ai + ag) * t + velocity.
self morphPosition:
(0.5 * (ai + ag) * t squared)
+ (velocity * t)
+ self morphPosition.
velocity := newVelocity.
"Are we out of screen? If so we move the mobile to the other corner
and slow it down by a factor of 2"
(self isInOuterSpace and: [self isGoingOuterSpace]) ifTrue: [
velocity := velocity / 2.
self morphPosition: self morphPosition negated]
Ejemplo 6.6: Método update: de Mobile
Ahora debemos añadir los dos métodos para detectar cuándo un móvil se dirige al espacio profundo.
Pero primero definimos el método localBounds en cada uno de
nuestros objetos Morph. Devuelve una instancia Rectangle
definida en las coordenadas Morph por su origen y extensión:
SpaceWar>>localBounds ^ -300 @ -300 extent: 600 @ 600 CentralStar>>localBounds ^ Rectangle center: 0 @ 0 extent: self morphExtent Mobile>>localBounds ^ Rectangle encompassing: self class vertices
Ejemplo 6.7: Límites de nuestros objetos Morph
Mobile>>isInOuterSpace "Is the mobile located in the outer space? (outside of the game play area)" ^ (owner localBounds containsPoint: self morphPosition) not Mobile>>isGoingOuterSpace "is the mobile going crazy in the direction of the outer space?" ^ (self morphPosition dotProduct: velocity) > 0
Ejemplo 6.8: Comprobar que un mobile está «fuera del espacio»
Como puedes ver, estos métodos de prueba son sencillos y cortos. Al
escribir código Cuis-Smalltalk, esto es algo que valoramos mucho y no dudamos
en dividir un método largo en varios métodos pequeños. Mejora la
legibilidad y la reutilización del código. El mensaje
#containsPoint: pregunta al rectángulo receptor si el punto del
argumento se encuentra dentro de su forma.
Cuando se actualiza un móvil, se actualizan su posición y velocidad. Sin
embargo, las subclases Mobile, SpaceShip o
Torpedo pueden necesitar actualizaciones específicas
adicionales. En la programación orientada a objetos existe un mecanismo
especial denominado sobrescritura (overriding) para
lograrlo.
Observa la definición de Torpedo>>update::
See the Torpedo>>update: definition:
Torpedo>>update: t "Update the torpedo position" super update: t. "orientate the torpedo in its velocity direction, nicer effect while inaccurate" self heading: (velocity y arcTan: velocity x). lifeSpan := lifeSpan - 1. lifeSpan isZero ifTrue: [owner destroyTorpedo: self]. acceleration > 0 ifTrue: [acceleration := acceleration - 500]
Aquí, el método update: está especializado para las necesidades
específicas del torpedo. El cálculo mecánico realizado en
Mobile>>update: sigue utilizándose para actualizar la posición
y la velocidad del torpedo: esto se hace mediante super update: t. Ya
hemos hablado de super. En el contexto de Torpedo>>update:,
significa buscar un método @.msg update:@ en la clase padre de
Torpedo, en la clase padre de esa clase, y así sucesivamente
hasta encontrar el método; si no se encuentra, se señala un error de
Message Not Understood (Mensaje no comprendido).
Entre los comportamientos añadidos específicos, la orientación del torpedo a lo largo de su vector de velocidad es inexacta, pero tiene un aspecto agradable. Para orientar el torpedo adecuadamente, ajustamos su rumbo con el rumbo de su vector de velocidad.
El control de la vida útil, la secuencia de autodestrucción y la aceleración del motor también se gestionan de forma específica. Cuando se dispara un torpedo, la aceleración de su motor es enorme, pero luego disminuye rápidamente.
Con el navegador del sistema apuntando al método
Torpedo>>update:, observa el botón inheritance. Es de
color verde claro, lo que indica que el mensaje también se envía a
super. Esto es un recordatorio de que el método proporciona un
comportamiento especializado. La información sobre herramientas del
botón explica el significado de los colores resaltados en el texto del
método. Al pulsar el botón inheritance, se pueden explorar
todas las implementaciones del método update: dentro de esta
cadena de herencia.
Figura 6.14: Botón inheritance de update:
Ya hemos visto un ejemplo de sobrescritura al inicializar una instancia
de nave espacial: véase Ejemplo 3.17. En el contexto de la
refactorización de nuestra clase, la sobrescritura de
initialize abarca toda la jerarquía Mobile:
Mobile>>initialize super initialize. color := Color gray. velocity := 0 @ 0. acceleration := 0 SpaceShip>>initialize super initialize. self resupply Torpedo>>initialize super initialize. lifeSpan := 500. acceleration := 3000
Ejemplo 6.9: Sobreescribir Initialize en la jerarquía Mobile
hierarchy
Observa cómo cada clase solo es responsable de la inicialización de su estado específico:
super initialize y, a continuación, se reabastece la nave con
combustible y torpedos:
SpaceShip>>resupply fuel := 500. torpedoes := 10
Los comportamientos específicos de cada móvil se establecen con métodos
adicionales. El SpaceShip viene con sus métodos de control que
ya describimos anteriormente en Ejemplo 5.8 y Ejemplo 5.9,
por supuesto no hay ninguno para un Torpedo.
Otro comportamiento específico importante es cómo se dibuja cada tipo de
Mobile en el juego, lo cual se tratará en el próximo capítulo
sobre los fundamentos de Morph.
Un PlacedMorph es un tipo de Morph con un
atributo location suplementario, por lo que se le puede indicar que se
mueva, se escale y gire en la pantalla.
Por lo tanto, un torpedo debe tener masa.