This game shows a field of moving cars of different sizes and colors. The player controls the red car (initially at the top-left corner) through the mouse cursor. The objective is to move the red car within the time limit with as few collisions as possible. The lower the speed limit, the slower the cars. The game runs until the time limit is over (in minutes). You will lose if the number of collisions during that time is greater than the time elapsed in seconds. You win if you survive the time limit. If the red car is hit, it shrinks to a small black car. In order to revive it, move the cursor to one of the borders of the board. After a certain number of collisions, clone cars appear. Here is a typical game screen:
This is our first example of a web app where all code is assembled in a collision_clones
library that resides in its own lib
folder. Because it is also stored in the packages
folder, it can be imported (line (1)
) in the startup script webcollision_clones.dart
, where a new object of the class Board
is created:
import 'package:collision_clones/collision_clones.dart'; (1) main() { new Board(); }
Now, look at pubspec.yaml
:
dependencies: browser: any simple_audio: git: https://github.com/johnmccutchan/simpleaudio.git
We see that the simple_audio
library, which was briefly discussed at the end of the Adding audio to a web page section, is also imported in the libcollision_clones.dart
library file along with all the other constituent packages and source files:
library collision_clones; import 'dart:html'; import 'dart:async'; import "dart:json"; import 'dart:math'; import 'package:simple_audio/simple_audio.dart'; part 'view/game_board.dart'; part 'model/cars.dart'; part 'model/score.dart'; part 'sound/audio.dart'; part 'util/color.dart'; part 'util/random.dart';
Again, the view
class is called Board
(in viewgame_board.dart
) and its instantiation starts and animates the game. The viewable objects in this game are the cars, and specifically the red car. They have corresponding classes in libmodelcars.dart
and the Board
class contains List<Car>
and a variable redCar
. Because they share many properties, we let them both inherit from an abstract class Vehicle
:
abstract class Vehicle class Car extends Vehicle class RedCar extends Vehicle
The abstract
class contains all that is needed to draw a car in its constructor and the draw
method: getting the canvas context, calculating a random position (x, y) and color code, and the methods on the canvas context we saw in Chapter 5, Handling the DOM in a New Way, are put to use. The draw
method is given as follows:
draw() { context ..beginPath() ..fillStyle = colorCode ..strokeStyle = 'black' ..lineWidth = 2; roundedCorners(context, x, y, x + width, y + height, 10); context ..fill() ..stroke() ..closePath(); // wheels context ..beginPath() ..fillStyle = '#000000' ..rect(x + 12, y - 3, 14, 6) ..rect(x + width - 26, y - 3, 14, 6) ..rect(x + 12, y + height - 3, 14, 6) ..rect(x + width - 26, y + height - 3, 14, 6) ..fill() ..closePath(); }
Every Car
object has a different speed specified in its constructor: a random number that is lower than the speed limit.
Car(canvas, speedLimit) : super(canvas) { var speedNumber = int.parse(speedLimit); dx = randomNum(speedNumber); dy = randomNum(speedNumber); }
The Car
class contains a move
method, which is called from displayCars()
in Board
, that changes the position (line (1)
), taking into account that cars must bounce from the borders (line(4)
). In line (2)
we test if the car referred to by this has had a collision with the red car, and in line (3)
, we test if this car has had a collision with the red car:
move(RedCar redCar, List<Car> cars) { x += dx; (1) y += dy; if (redCar.big) { redCar.collision(this); (2) } for (Car car in cars) { (3) if (car != this) { car.collision(this); } } if (x > canvas.width || x < 0) dx = -dx; (4) if (y > canvas.height || y < 0) dy = -dy; }
The red car creates an AudioManager
object (line (1)
in the following code snippet) in its constructor to make a sound when it collides with another car. The red car also contains some Booleans to determine whether it is in its big or small state and when it is movable. The onMouseDown
and onMouseMove
events are defined on document
property of canvas, and are also there in the red car constructor:
RedCar(canvas) : super(canvas) { audioManager = new Audio().audioManager; (1) colorCode = bigColorCode; width = bigWidth; height = bigHeight; canvas.document.onMouseDown.listen((MouseEvent e) { (2) movable = !movable; if (small) { bigger(); } }); canvas.document.onMouseMove.listen((MouseEvent e) { (3) if (movable) { x = e.offset.x - 35; y = e.offset.y - 35; if (x > canvas.width) { bigger(); x = canvas.width - 20; } // some code left out for brevity
The first event handler (line (2)
) makes the red car movable after a click on the Play button at the beginning of the game or after a click on the Stop button. The second handler (line (3)
) makes sure the red car is contained within the board's dimensions and that it is revived at the limits of the board after a collision. When the red car collides with another car, the collision
method in RedCar
invokes smaller()
:
smaller() { if (big) { small = true; audioManager.playClipFromSourceIn(0.0, 'game', 'collision'), colorCode = smallColorCode; width = smallWidth; height = smallHeight; collisionCount++; } }
This plays a typical collision sound from websoundcollision.ogg
. We have added an Audio
class (in libsoundaudio.dart
) that wraps functionality from the simple_audio
library:
class Audio { AudioManager _audioManager; Audio() { _audioManager = new AudioManager('${demoBaseUrl()}/sound'), AudioSource audioSource = _audioManager.makeSource('game'), audioSource.positional = false; AudioClip collisionSound = _audioManager.makeClip('collision', 'collision.ogg'), collisionSound.load(); } // code left out AudioManager get audioManager => _audioManager; }
The Board
class constructs a Score
object, a Red Car
object, and the other cars. Then displays them in displayCars()
every INTERVAL
milliseconds in line (1)
. In line (2)
, the score is updated, displayed, and stored in local storage every ACTIVE
millisecond (see save()
and load()
in the class Score
):
Board() { var bestScore = new Score(); // code left out redCar = new RedCar(canvas); cars = new List(); for (var i = 0; i < carCount; i++) { var car = new Car(canvas, score.currentSpeedLimit); cars.add(car); } // code left out // active play time: new Timer.periodic(const Duration(milliseconds: 1000), (t) { if (!stopped && redCar.big) { (2) // code left out }
Here is a portion of the displayCars()
code:
displayCars() { // code left out clear(); // nested function ! for (var i = 0; i <cars.length; i++) { cars[i].move(redCar, cars); cars[i].draw(); } redCar.draw(); // code left out
The Score
class also contains supporting code to keep track of the counters. That's it, you can find some suggestions for change or improvement in doc odo.txt
.
An important improvement can still be made. The Car
class contains both state information (position, color, and so on) as well as the way to draw itself. The model should not concern itself with the user interface. This separation is achieved in the car_collisions
variant where everything related to drawing is moved to the view
class Board
. Also, in this variant the built-in AudioElement
class is used. Compare both versions and see how the separation makes the project much easier to understand and maintain.
18.217.206.112