After defining the classes involved in the game design, we now define several states of these classes:
SpaceWar
instance representing the gameplay needs to
know about the centralStar
, the ships
, the
fired torpedoes
, and its color
.
CentralStar
has a mass
state. It is
necessary to compute the gravity force applied to a given ship.
SpaceShip
instance knows about its name
,
its position
coordinates, its heading
angle,
its velocity
vector, its fuel
gauge, its
count of the available torpedoes
, its mass
and its acceleration
engine boost.
Torpedo
has position
,
velocity
and lifeSpan
states.
We need to explain the mathematical nature of these states, and then discuss their object representation in the instance variables of our classes.
In the following sections, to ease reading we will write “the variable
myVar
is aString
” instead of the correct but cumbersome “the instance variablemyVar
is a reference to aString
instance”.
SpaceWar
¶This object is the entry into the game. We want a meaningful class name. Its instance variables are the involved protagonists of the game:
centralStar
is the unique CentralStar
of
the game play. We need to know about it to request its mass.
ships
is a collection of the two player ships. It
is an Array
instance, its size is fixed to two elements.
torpedoes
is a collection of the fired torpedoes
in the gameplay. As this quantity is variable, a dynamic
OrderedCollection
makes sense.
CentralStar
¶Its unique instance variable, mass
, is a number, most likely
an Integer
.
SpaceShip
¶The spaceship is the most complex object, some clarifications regarding its variables are needed.
name
is a String
.
position
is a 2D screen coordinate, a
location. Smalltalk uses the Point
class to represent
such objects. It understands many mathematics operations as
operations on vectors; very useful for mechanical calculations.
A point is easily instantiated with the binary message #@
send
to a number with another number as its argument: 100 @
200
returns a Point
instance representing the
coordinates (x;y) = (100;200).
The ship’s position
is regularly recomputed according to
the law of the Galilean reference frame. The computation depends on
the ship’s velocity, it’s current engine boost, and the gravity pull
of the central star.
heading
is an angle in radians, the direction
where the ship’s nose is pointing. It is therefore a Float
number. At the game’s start, the ships are oriented to the top, the
heading
value is then -pi/2 radians; the oy axis is
oriented from the top to the bottom of the screen.
velocity
is the vector representing the
instantaneous speed of the ship. It is a Point
instance.
fuel
is the gauge, as long as it is not zero, the
player can ignite the ship’s rocket engine to provide acceleration to move
around and to counter the central star’s gravity pull. It is an
integer number.
torpedoes
is the quantity of available torpedoes
the player can fire. It is an Integer
number.
mass
is an Integer
representing the ship
mass.
acceleration
is the intrinsic ship acceleration
norm provided when the ship’s rockets are ignited. It is therefore
an Integer
number.
A few words regarding the Euclidean coordinates: the origin of our orthonormal frame is the central star, its first vector is oriented toward the right of the screen, and the second one towards the top of the screen. This choice eases the computation of the ship’s acceleration, velocity and position. More on this below.
Torpedo
¶A torpedo is launched or “fired” from a ship with an initial velocity related to the ship’s velocity. Once the torpedo life span counter reaches zero, it self-destructs.
position
is a 2D screen coordinate, a
Point
instance. Unlike the ship, it does not accelerate based
on the gravity pull of the central star. Indeed, a torpedo does not
come with a mass state. For our purposes, it is essentially
zero. Its position over time only depends on the torpedo velocity
and its initial acceleration.
heading
is an angle in radians, the direction
where the torpedo nose is pointing. Its value matches the ship
heading when fired, it is therefore a Float
number too.
velocity
is a vector representing the
instantaneous speed of the torpedo. It is constant over the torpedo’s
lifespan. Again velocity is kept as a Point
instance.
lifeSpan
is an integer number counter, when it
reaches zero the torpedo self-destructs.
In the previous chapter, we explained how to define the four classes
SpaceWar
, CentralStar
, SpaceShip
and
Torpedo
. In this section, we will add to these definitions
the instance variables – states – discussed above.
To add the variables to the Torpedo
class, from the Browser,
select this class. Next, add the variable names to the
instanceVariableNames:
keyword, separated by one space
character. Finally, save the updated class definition with
Ctrl-s shortcut:
Object subclass: #Torpedo instanceVariableNames: 'position heading velocity lifeSpan' classVariableNames: '' poolDictionaries: '' category: 'Spacewar!'
Add the instance variables we discussed earlier to the
SpaceWar
,CentralStar
andSpaceShip
classes.
Some of these states need to be accessed from other entities:
ship name: 'The needle'
.
star mass * ship mass
.
To write these behaviors in the Browser, first select the class then
the method category you want – when none, select -- all
--
.
In the code pane below appears a method template:
messageSelectorAndArgumentNames "comment stating purpose of message" | temporary variable names | statements
It describes itself as:
The getter mass
on SpaceShip
is written as:
SpaceShip>>mass ^ mass
The SpaceShip>>
part is not valid code and should not be
written in the Browser. It is a text convention to inform the reader
the subsequent method is from the SpaceShip
class.
Write the
SpaceShip
getter messages for itsposition
,velocity
andmass
attributes.
Some instance variables need to be set from another entity, so a setter keyword message is necessary. To set the name of a space ship we add the following method:
SpaceShip>>name: aString name := aString
The := character is an assignment, it means the
name
instance variable is bound to the aString
object.
To type in this symbol type _ then space,
Cuis-Smalltalk will turn it into the left arrow symbol. Alternatively write
name := aString
. One might pronounce := as “gets”.
Since name
is an instance variable, each instance method
knows to use the box for the name. The meaning here is that we are
placing the value of the aString
argument into the
instance’s box called name
.
Since each instance variable box can hold an object of any class, we
like to name the argument to show that we intend that the
name
variable should hold a string, an instance of the
String
class.
Ship
position
andvelocity
, as well as torpedoheading
will need to be set at game start-up or when a ship jumps in hyperspace. Write the appropriate setters.
Observe how we do not have a setter message for the spaceship
mass
attribute. Indeed, it does not make sense to change
the mass of a ship from another object. In fact, if we consider both
player ships to be of equal mass, we should remove the mass
variable and edit the mass
method to return a literal number:
SpaceShip>>mass ^ 1
On the other hand, we could also consider the mass
to
depend on the consumed fuel and torpedoes. After all, 93 % of
Saturn V rocket’s mass was made up of its fuel. We will discuss
more about that later in the chapter about
refactoring.
A spaceship controlled by the player understands messages to adjust its direction and acceleration17:
Direction. The ship’s heading is controlled with the
#left
and #right
messages. The former decrements the
heading
by 0.1 and the latter increments it by 0.1.
Write two methods named
left
andright
to shift the ship heading of 0.1 according to the indications above.
Acceleration. When the #push
message is sent to
the ship, the engines are ignited, and an internal acceleration of 10
units of acceleration are applied to the ship. When the #unpush
message is sent, the acceleration stops.
Write two methods named
push
andunpush
to adjust the ship’s inner acceleration according to the indications above.
When an instance is created, for example, SpaceShip new
, it
is automatically initialized: the message #initialize
is sent to
the newly created object and its matching initialize
instance
side method is called.
The initializing process is useful to set the default values of the instance variables. When we create a new space ship object we want to set its default position, speed, and acceleration:
SpaceShip>>initialize super initialize. velocity := 0 @ 0. position := 100 @ 100. acceleration := 0
In the method Example 3.17, observe the first line
super initialize
. When a message is sent to
super
, it refers to the superclass of the class’s method
using super
. So far, the SpaceShip
parent class is
Object
, therefore the Object>>initialize
method is
called first for initialization.
When created, a spaceship is positioned to the top and right of the central star. It has no velocity nor internal acceleration – only the gravity pull of the central star. Its nose points in direction of the top of the game display.
You probably noticed there is no code to initialize the
heading
, something like heading := Float
halfPi negated
to orient the ship in the direction of the top of the
screen. The truth is we don’t need the heading
attribute,
this information will be provided by the class Morph
used
later as a parent class of SpaceShip
and Torpedo
. At
this time, the heading
variable will be removed and we
will define the heading behavior with the appropriate heading
and heading:
methods.
Write the method to initialize the central star with 8000 units of mass.