6.6 Spacewar! Morphs

6.6.1 All Morphs

Previously we defined the actors of the game as subclasses of the very general Object class (See Example 3.14). However the game play, the central star, the ships and the torpedoes are visual objects, each with a dedicated graphic shape:

Therefore it makes sense to turn these actors into kinds of Morphs, the visual entity of Cuis-Smalltalk. To do so, point a System Browser to the class definition of each actor, replace the parent class Object by PlacedMorph20, then save the class definition with Ctrl-s.

For example, the torpedo class as seen in Example 3.14 is edited as:

PlacedMorph subclass: #Torpedo
   instanceVariableNames: 'position heading velocity lifeSpan'
   classVariableNames: ''
   poolDictionaries: ''
   category: 'Spacewar!'

Moreover, a placed Morph already knows about its position and orientation on screen – it can be dragged and moved in the screen and rotated with the mouse cursor. Therefore the position and heading instance variables are redundant and should be removed. For now we keep it, it will be removed later when we will know how to replace each of its use cases with its appropriate Morph counterpart.

 CuisLogo Edit SpaceWar, CentralStar and SpaceShip to be subclasses of the PlacedMorph class.

Exercise 6.1: Make all Morphs

As explained in the previous sections of this chapter, a morph can be embedded within another morph. In Spacewar!, a SpaceWar morph instance presenting the game play, it is the owner of the central star, space ship and torpedo morphs. Put in other words, the central star, space ships and torpedoes are submorphs of a SpaceWar morph instance.

The SpaceWar>>initializeActors code in Example 4.17 is not complete without adding and positioning the central star and space ships as submorphs of the Spacewar! game play:

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

Example 6.3: Complete code to initialize the Spacewar! actors

There are two important messages: #addMorph: and #morphPosition:. The former asks to the receiver morph to embed its morph argument as a submorph, the latter asks to set the receiver coordinates in its owner’s reference frame. From reading the code, you deduce the origin of the owner reference frame is its middle, indeed our central star is in the middle of the game play.

There is a third message not written here, #morphPosition, to ask the coordinates of the receiver in its owner’s reference frame.

Remember our discussion about the position instance variable. Now you clearly understand it is redundant and we remove it from the SpaceShip and Torpedo definitions. Each time we need to access the position, we just write self morphPosition and each time we need to modify the position we write self morphPosition: newPosition. More on that later.

6.6.2 The art of refactoring

In our newtonian model we explained the space ships are subjected to the engine acceleration and the gravity pull of the central star. The equations are described in Figure 2.4.

Based on these mathematics, we wrote the SpaceShip>>update: method to update the ship position according to the elapsed time – see Example 4.19.

So far in our model, a torpedo is not subjected to the central star’s gravity pull nor its engine acceleration. It is supposing its mass is zero which is unlikely. Of course the Torpedo>>update: method is simpler than the space ship counter part – see Example 4.18. Nevertheless, it is more accurate and even more fun that the torpedoes are subjected to the gravity pull21 and its engine acceleration; an agile space ship pilot could use gravity assist to accelerate a torpedo fired with a path close to the central star.

What are the impacts of these considerations on the torpedo and space ship entities?

  1. They will share common states as the mass, the position, the heading, the velocity and the acceleration.
  2. They will share common behaviors as the necessary computations to update the position, direction, gravity pull and velocity.
  3. They will have different states: a torpedo has a life span state while a space ship has fuel tank capacity and torpedoes stock states.
  4. They will have different behaviors: a torpedo self destructs when its life span expires, a space ship fires torpedoes and accelerates as long as its fuel tank and its torpedoes count are not zero.

Shared state and behaviors suggest a common class. Unshared states and behaviors suggests specialized subclasses which embody the differences. So let us “factor out” the shared elements of the SpaceShip and Torpedo classes into a common ancestor class; one more specialized than the Morph class they currently share.

Doing such analysis on the computer model of the game is part of the refactoring effort to avoid behavior and state duplications while making more obvious the common logic in the entities. The general idea of code refactoring is to rework existing code to make it more elegant, understandable and logical.

To do so, we will introduce a Mobile class, a kind of PlacedMorph with behaviors specific to a mobile object subjected to accelerations. Its states are the mass, position, heading, velocity and acceleration. Well, as we are discussing refactoring, the mass state does not really makes sense in our game, indeed our mobile’s mass is constant. We just need a method returning a literal number and we can then remove the mass instance variable. Moreover, as explained previously, a PlacedMorph instance already know about its position and heading, so we also remove these two attributes, although there are common behaviors to a Space ship and a torpedo.

It results in this Mobile definition:

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

Example 6.4: Mobile in the game play

 CuisLogo What should be the refactored definitions of the SpaceShip and Torpedo classes?

Exercise 6.2: Refactoring SpaceShip and Torpedo

The first behaviors we add to our Mobile are its initialization and its mass:

Mobile>>initialize
  super initialize.
  velocity := 0 @ 0.
  acceleration := 0
        
Mobile>>mass
  ^ 1

The next methods to add are the ones relative to the physical calculations. First, the code to calculate the gravity acceleration:

Mobile>>gravity
"Compute the gravity acceleration vector"
   | position |
   position := self morphPosition.
   ^ -10 * self mass * owner starMass / (position r raisedTo: 3) * position

Example 6.5: Calculate the gravity force

This method deserves a few comments:

The method to update the mobile position and velocity is mostly the same as in Example 4.19. Of course the SpaceShip>>update: and Torpedo>>update: version must be both deleted. Below is the complete version with the morph’s way of accessing the mobile’s position:

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]

Example 6.6: Mobile’s update: method

Now we should add the two methods to detect when a mobile is heading off into deep space.

But first we define the method morphLocalBounds in each of our Morph objects. It returns a Rectangle instance defined in the Morph coordinates by its origin and extent:

SpaceWar>>morphLocalBounds
   ^ -300 @ -300 extent: 600 @ 600

CentralStar>>morphLocalBounds
   ^ Rectangle center: 0 @ 0 extent: self morphExtent

Mobile>>morphLocalBounds
   ^ Rectangle encompassing: self class vertices

Example 6.7: Bounds of our Morph objects

Mobile>>isInOuterSpace
"Is the mobile located in the outer space? (outside of the game
play area)"
   ^ (owner morphLocalBounds containsPoint: self morphPosition) not

Mobile>>isGoingOuterSpace
"is the mobile going crazy in the direction of the outer space?"
   ^ (self morphPosition dotProduct: velocity) > 0

Example 6.8: Test when a mobile is “spaced out”

As you see, these test methods are simple and short. When writing Cuis-Smalltalk code, this is something we appreciate a lot and we do not hesitate to cut a long method in several small methods. It improves readability and code reuse. The #containsPoint: message asks the receiver rectangle whether the point in argument is inside its shape.

When a mobile is updated, its position and velocity are updated. However the Mobile subclasses SpaceShip or Torpedo may need additional specific updates. In object oriented programming there is this special mechanism named overriding to achieve this.

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]

Here the update: method is specialized to the torpedo specific needs. The mechanical calculation done in Mobile>>update: is still used to update the torpedo position and velocity: this is done by super update: t. We already discussed super. In the context of Torpedo>>update: it means search for an update: method in Torpedo’s parent class, that class’s parent and so on until the method is found, if not a Message Not Understood error is signalled.

Among the specific added behaviors, the torpedo orientation along its velocity vector is inaccurate but nice looking. To orient accordingly the torpedo, we adjust its heading with its velocity vector heading.

The life span control, the self-destruction sequence, and the engine acceleration are also handled specifically. When a torpedo is just fired, its engine acceleration is huge then it decreases quickly.

With the System Browser pointed to the Torpedo>>update: method, observe the inheritance button. It is light green, which indicates the message is sent to super too. This is a reminder the method supplies a specialized behavior. The button tool tip explains the color hilight meanings within the method’s text. When pressing the inheritance button, you browse all implementations of the update: method within this inheritance chain.

ch06-20-TorpedoUpdateInheritance

Figure 6.14: Update’s inheritance button

We already met an example of overriding when initializing a space ship instance – see Example 3.17. In the context of our class refactoring, the initialize overriding spans the whole Mobile hierarchy:

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

Example 6.9: Initialize overriding in the Mobile hierarchy

Observe how each class is only responsible of its specific state initialization:

  1. SpaceShip. Its mechanical states are set with the super initialize and then the ship is resupplied with fuel and torpedoes:
    SpaceShip>>resupply
       fuel := 500.
       torpedoes := 10
    
  2. Torpedo. Inherited mechanical states initialized; add self-destroy sequence initialization and acceleration adjusted to mimic the torpedo boost at fire up.

The behaviors specific to each mobile is set with additional methods. The SpaceShip comes with its control methods we already described previously in Example 5.8 and Example 5.9, of course there is none for a Torpedo.

Another important specific behavior is how each kind of Mobile is drawn in the game play, this will be discussed in a next chapter on the fundamentals of Morph.


Footnotes

(20)

A PlacedMorph is a kind of Morph with a supplementary location attribute; so it can be instructed to move, to scale and to rotate in the screen.

(21)

So a torpedo should come with a mass.