4.5 SpaceWar! collections

4.5.1 Instantiate collections

Whenever you need to deal with more than one element of the same nature – instances of the same class – it is a clue to use a collection to hold them. Moreover, when these elements are of a fixed quantity, it indicates more precisely you want to use an Array instance. An Array is a collection of fixed size. It can not grow nor shrink.

When this quantity is variable, you want to use an OrderedCollection instance. It is a collection of variable size, it can grow or shrink.

SpaceWar! is a two-player game, there will always be two players and two spaceships. We use an Array instance to keep a reference to each spaceship.

Each player can fire several torpedoes; therefore the gameplay holds zero or more torpedoes – hundreds if we decide so. The torpedoes quantity is variable, we want to use an OrdredCollection instance to keep track of them.

In the SpaceWar class, we already defined two instance variables ships and torpedoes. Now, we want an initializeActors method to set up the game with the involved actors – central star, ships, etc. Part of this initialization is to create the necessary collections.

See below an incomplete implementation of this method:

SpaceWar>>initializeActors
   centralStar := CentralStar new.
   ../..
   ships first 
      position: 200 @ -200;
      color: Color green. 
   ships second
      position: -200 @ 200;
      color: Color red

Example 4.17: Incomplete game initialization

 CuisLogo The example above does not show the creation of the ships and torpedoes collections. Replace “../..” with lines of code where these collections are instantiated and if necessary populated.

Exercise 4.17: Collections to hold the ships and torpedoes

4.5.2 Collections in action

The spaceship and the torpedo objects are responsible for their internal states. They understand the #update: message to recompute their position according to the mechanical laws.

A fired torpedo has a constant velocity, no external forces are applied to it. Its position is linearly updated according to the time elapsed. The t parameter in the #update: message is this time interval.

Torpedo>>update: t
"Update the torpedo position"
   position := velocity * t + position.
   ../..

Example 4.18: Torpedo mechanics

A spaceship is put under the strain of the star’s gravity pull and the acceleration of its engines. Therefore its velocity and position change according to the mechanical laws of physics.

SpaceShip>>update: t
"Update the ship position and velocity"
   | ai ag newVelocity |
   "acceleration vectors"
   ai := acceleration * self direction.
   ag := self gravity.
   newVelocity := (ai + ag) * t + velocity.
   position := (0.5 * (ai + ag) * t squared) + (velocity * t) + position.
   velocity := newVelocity.
   ../..

Example 4.19: Space ship mechanics

 note Remember that Smalltalk does not follow the mathematics precedence of arithmetic operators. These are seen as ordinary binary messages which are evaluated from the left to the right when there is no parenthesis. For example, in the code fragment ...(velocity * t)..., the parenthesis are mandatory to get the expected computation.

Observe in this previous method how the direction and the gravity are defined in two specific methods.

The #direction message asks for the unit vector representing the nose direction of the spaceship:

SpaceShip>>direction
"I am a unit vector representing the nose direction of the mobile"
   ^ Point rho: 1 theta: self heading

Example 4.20: Space ship direction method

The #gravity message asks for the gravity vector of the spaceship is subjected to:

SpaceShip>>gravity
"Compute the gravity acceleration vector"
   | position |
   position := self morphPosition.
   ^ [-10 * self mass * self starMass / (position r raisedTo: 3) * position]
      on: Error do: [0 @ 0]

Example 4.21: Space ship gravity

Observe the message #starMass sent to the spaceship herself. We, as a spaceship, have not yet figured out how to ask the central star for its stellar mass. Our starMass method can just return, for now, the number 8000.

The gameplay is the responsibility of a SpaceWar instance. At a regular interval of time, it refreshes the states of the game actors. A stepAt: method is called at a regular interval of time determined by the stepTime method:

SpaceWar>>stepTime
"millisecond"
   ^ 20

SpaceWar>>stepAt: millisecondSinceLast
   ../..
   ships do: [:each | each unpush].
   ../..

Example 4.22: Regular refresh of the gameplay

In the stepAt: method, we intentionally left out the details to update the ship and torpedo positions. Note: each ship is sent regularly an #unpush message to reset its previous #push acceleration.

 CuisLogo Replace the two lines “../..” with code to update the ships and the torpedoes’ positions and velocities.

Exercise 4.18: Update all ships and torpedoes

Among other things, the gameplay handles the collisions between the various protagonists. Enumerators are very handy for this.

Ships are held in an array of size 2, we just iterate it with a #do: message and a dedicated block of code:

SpaceWar>>collisionsShipsStar
   ships do: [:aShip | 
      (aShip morphPosition dist: centralStar morphPosition) < 20 ifTrue: [
         aShip flashWith: Color red.
         self teleport: aShip]
   ]

Example 4.23: Collision between the ships and the Sun