|
Of course, other classes are required in our design,
which also follow the MVC pattern where possible.
Player objects are created for each player connected
to the game and are stored in a player list. The
application broadcasts changes in the player list
to all clients so that all players can see changes
in player status immediately, such as players dropping
out of the game. A separate player controller object
handles player connections, requests to start the
first game, restart a game, or disconnect from
a game in progress. The player controller object
can easily check the state of the game and respond
to user requests accordingly. For example, once
the game starts, the player controller will not
allow any new players to join the game. The player
view is a visible panel in the client that allows
the user to connect to a game, leave a game, and
see information about other players.
Finally, our game has an audio chat system to
allow players to discuss the game as it progresses.
Players can decide who will hear their remarks
and can mute sound from any player. We built the
audio chat on top of information in the player
and player list; the player controller controls
it.
The following section provides a more detailed
description of some of the classes that make up
the game and how they work.
The GameModelClass
The game model maintains all the state information
about the game and provides methods that correctly
manipulate the squares in the board. It contains
information about the size of the board, an array
of square objects, and anything else it needs to
initialize, play, and end a game. The game logic
is also contained within this object. The game model
contains an instance of the BoardClass where it processes
most of the game logic. For example, when it receives
a request to uncover a square, the game model will
first check if the square is already uncovered. If
it isn't uncovered, it will mark who has uncovered
it (by color) and then uncover the square. If the
square is blank it will recursively uncover all the
neighboring squares until no adjacent blank squares
remain. When the uncovering process finishes, the
application broadcasts a message listing the results.
There are a number of ways to implement the game
board and recursive uncovering of squares. In this
game, the board is a simple one-dimensional array.
As an example of how this works, you can represent
a simple three-by-three board with an array with
9 slots numbered 0 through 8 that map to a board
as illustrated here:
To begin a game, you must randomly place bombs
on the board. There are a number of ways to do
this; here, we created a temporary array containing
the number of each square:
As positions in the temporary array are randomly
selected as bomb locations, two things happen.
First, you add the bomb location to another array
called the map array by placing an x in the slot
and placing the number of bombs in the surrounding
squares. In this example slot number 5 was randomly
selected to hold a bomb and the surrounding squares
given the value 1:
Which represents this board layout:
Second, you remove slot number five from the temporary
array using the Array.splice() method:
tempArray.splice(randomCell, 1);
Reducing the size of the temporary array this
way makes it easy to choose another random cell
without a bomb in it:
randomCell = Math.floor(Math.random()* tempArray.length);
After you repeat this process and place all the
bombs, users could begin the game. However, there
is a problem with converting minesweeper to a multi-player
game. In the single-user version, the first square
uncovered cannot contain a mine. One way to implement
this is to secretly move the mine to another square
if the user clicks on a mine. In a multi-user version
it didn't seem fair to let everyone try to click
on a square with the possible result that after
one square was uncovered others would contain a
mine and end the game for those users. Instead
we chose to uncover at least one square when the
game begins. That way each player has a clear starting
position to asses. Describing the entire game is
beyond the scope of this tutorial. However, some
methods of interest in the BoardClass are:
- populateBomb - uses a temporary array to randomly
choose bomb locations
- getNeighbors - given a square location returns
a list of neighboring squares
- preOpen - opens a square and its neighbors
so that all players have somewhere to start
- openSpaces - recursively opens spaces
The source for the GameModelClass is in the GameModelClass.asc
file while the source for BoardClass is in the
BoardClass.asc file. Also see SquareClass.asc.
The GameModelClass coordinates user interactions
and updates the views; it bases the updates on
the current state of the game within the board
object. The most complex part of this process is
what the GameModelClass does when it receives an
openSquare message from a movie. (See the openSquare
method of the GameModel Class.) This process is
complicated because many users may click on an
uncovered square within a few milliseconds of each
other. However, the first openSquare message that
arrives at the server may not represent the first
click in real-time. To understand this scenario,
imagine two players playing the game. Player A
connects to the server from outside Ryerson's campus
and has an average network latency of 120 milliseconds,
or just longer than one tenth of a second. Player
B is on campus and experiences an average latency
of only 10 milliseconds or one hundredth of a second.
The following sequence of events illustrates how
an unfair situation could happen:
| Event Sequence |
Time of Event (milliseconds) |
Description |
| 1 |
1000 |
Player A clicks on an unopened
square one second into the game. |
| 2 |
1100 |
Player B clicks on the same
square one tenth of a second later. |
| 3 |
1110 |
Player B's openSquare request
arrives at the server 10 milliseconds later,
which uncovers the square. |
| 4 |
1120 |
Player A's openSquare request
arrives at the server 120 milliseconds after
it was sent but the square was already uncovered
by Player B. |
In this unfair situation, player B gets credit
for opening a square even though player A clicked
on it first in real-time. Off-campus users may
experience much higher network latencies, which
introduces an unfair disadvantage due to the system.
To reduce unfairness, the application measures
each client's network latency and estimates the
real-time for each user's click on an unopened
square. If user A's request to open a square arrives
after the square has already been opened, user
A is assigned credit for opening the square. In
the current version of the game, we only measure
network latency by making a remote method call
after getting a current date/time. The server simply
returns from the method call and compares a new
date/time to the old one. The application determines
an approximate latency by dividing the round-trip
delay in half. We describe a better way of doing
this later in a related latency problem. In the
case where we need to correct who will get credit
for uncovering a square, we send a "correctGame" message
to all the movies:
application.playerList_so.send("correctGame",
this.gameBoard.mapArray[square],
square, removeFromPlayer);
All GameModelClass methods that communicate with
views use the send method of the playerList_so
shared object. For example the restart method contains
this statement:
application.playerList_so.send("reStartGame");
And here is another example from the openSquare
method:
application.playerList_so.send("openSpaces", str);
In this case, the openSpaces method of the player
list shared object will call the openSpaces method
of the game view. The string passed will contain
a comma separated lists of square locations and
the number to display in each of them.
GameControllerClass
An application may contain many MVC triads. Each
controller does not maintain state or store data.
It simply manages messages as they arrive. When there
are multiple MVC triads at work, the controllers
cooperate by passing messages to each other in order
to ensure that they update the correct model and
views from user events. In this game, using this
process turned out to be unnecessary. The game controller
simply passes all its messages directly to the game
model. In more complex applications cooperating controllers
may have some value. Here is a typical game controller
method:
GameControllerClass.prototype.requestToPreOpenBoard = function() { gameModel.preOpenBoard(); }
The source for the GameControllerClass is in the
GameControllerClass.asc file.
GameViewClass
The GameViewClass is a movie clip subclass that
shows the user what is happening in the game. It
is responsible for drawing the board and dealing
with messages sent from the game model. In turn the
GameViewClass contains another movie clip subclass:
the ClientSquareClass. A ClientSquareClass instance
draws each board square and contains the usual graphics
in its timeline for each state the square can appear:
covered, opened, number, bomb, and disappear.
When the openSquares method of this class is called,
it receives a string of squares to open and the
values to place in each square. It splits the string
into an array and alters the contents of each square
accordingly.
Each square is only responsible for reporting
when the user clicks on it. Here is the onRelease
handler of the ClientSquareClass:
square.onRelease = function(){
//start the clicking sound. click_sound.start(0, 1);
//send the info to the server. game_nc.call("openSquare", null, (this._parent._name).substring(3)); }
The source for the GameViewClass is in the GameViewClass.as
file. Also, see the ClientSquareClass.as file.
The Player List, Controller, and View Classes
Aside from the client object, which represents
each movie's network connection to the game, we
use a separate player object to keep track of each
player's name, score, network latency, display
color, and status on the server. (See the PlayerClass.asc
file.) The player objects are stored in the PlayerList
shared object. When players arrive or leave, the
application adds or deletes the entry in the PlayerList
through the PlayerListClass. Also, when a player's
score changes, the class updates the player objects
in the player list.
When the PlayerListClass adds or deletes a player
to the PlayerList shared object, each movie automatically
receives an onSync event. This is a feature of
remote shared objects and provides another way
that information can be broadcast to all the movies
connected to an application. For each movie to
receive and properly handle each onSync event,
you must define an onSync event handler method
for the shared object. The listing below shows
the handler for the PlayerList. It receives a list
of items that have changed whenever players add
or remove themselves. Lastly, the class examines
each item's code property to see what has happened
to the item and uses the item's name field to identify
the name of the shared object property that was,
in this case, added or deleted.
playerList_so.onSync = function(list) {
for (var i = 0; i<list.length; i++) {
// on change Add the new player.
if (list[i].code == "change") {
if (playerList_so.data[list[i].name].name == playerName) {
player = playerList_so.data[list[i].name];
}
var Tplayer = playerList_so.data[list[i].name];
playerView.addPlayer(list[i].name, Tplayer.color);
}
// on delete then remove the player
if (list[i].code == "delete") {
trace("Deleting: "+list[i].name);
playerView.info.info_txt.text += list[i].name + " Has left the game\n";
playerView.removePlayer(list[i].name);
}
}// end for
}
Whenever anything visible must change, the process
calls a method of the playerView object and in
turn the playerView object hides or shows the movie
clip that represents each player.
As was the case with the GameControllerClass,
the PlayerListControllerClass just passes messages
it receives to the player list which acts as the
model in the player list MVC triad. See the PlayerListClass.asc,
Player.asc, PlayerListController.asc and PlayerViewClass.as
files.
The Lobby and Games
If you have tried the game, it placed you in
a lobby before you could create or join a game.
The lobby is a separate and very simple communication
application. It lives in the flashcom/applications/mmsLobby
folder. When the app creates a game in a lobby,
nothing more than a name is created. The game is
created and lives in the flashcom/application/OOMS
folder when users attempt to connect to it. Each
game is a separate instance of an OOMS game. For
example players who play a game they name "challenge" actually
connect to a game instance through an RTMP request:
rtmp://hostname/OOMS/challenge. The main timeline
of the SWF file contains a lobby segment and a
game segment and all the code required to connect
to the lobby and the game.
|