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
Morph
s, 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 PlacedMorph
21, 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
,CentralStar
andSpaceShip
to be subclasses of thePlacedMorph
class.
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
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!'
What should be the refactored definitions of the
SpaceShip
andTorpedo
classes?
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
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]
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
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
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.
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
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.