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 PlacedMorph21, 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.
Edit
SpaceWar,CentralStarandSpaceShipto be subclasses of thePlacedMorphclass.
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.
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 pull22 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?
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
What should be the refactored definitions of the
SpaceShipandTorpedoclasses?
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:
self morphPosition returns a Point
instance, the position of the mobile in the owner reference frame,
owner is the SpaceWar instance
representing the game play. It is the owner – parent morph – of
the mobile. When asking #starMass, it interrogates its central
star mass and return its value:
SpaceWar>>starMass ^ centralStar mass
As a side benefit, we can remove the method starMass
defined earlier in the SpaceShip class.
position r, the #r message asks the radius
attribute of a point considered in polar coordinates. It is just its
length. It is the distance between the mobile and the central star.
* position really means multiply the previous
scalar value with a Point, hence a vector. Thus the returned
value is a Point, a vector in this context, the gravity
vector.
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 localBounds in each of
our Morph objects. It returns a Rectangle instance defined in
the Morph coordinates by its origin and extent:
SpaceWar>>localBounds ^ -300 @ -300 extent: 600 @ 600 CentralStar>>localBounds ^ Rectangle center: 0 @ 0 extent: self morphExtent Mobile>>localBounds ^ 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 localBounds 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.
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:
super initialize and then the ship is resupplied with
fuel and torpedoes:
SpaceShip>>resupply fuel := 500. torpedoes := 10
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.
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.
So a torpedo should come with a mass.