7. Using the Bluetooth API (JSR-82)
Multiplayer games are
one of the fastest growing areas in the games industry and one of the
great things about mobile phones is multiplayer games over Bluetooth.
Bluetooth is a wireless radio technology that operates in the 2.4 GHz
range over distances of up to 10 meters. These days, we use Bluetooth to
connect all types of peripheral devices so it is a great technology to
know how to use. Before proceeding, we recommend reading an excellent
series of articles on multiplayer games available from the Sun
Developers' Network, which gives a great background into a number of concepts.
Bluetooth can be
reasonably difficult to understand at first because, unlike most other
areas of networking, the actual topology of the network can be very
dynamic as new devices may come in and out of range at any time. Even
the process of establishing a connection between two Bluetooth devices
is unusual because connections are initiated from the server (or
'master') device to clients that listen on server sockets. Most examples
you find online mix UI code directly in with connection management code
and events, often making it difficult to focus on the key concepts.
The Bluetooth code included in our sample game has been taken directly from Forum Nokia. The main changes we have introduced are the
use of an observer class and simplification of the code to support only
two players. If you wish to learn about Bluetooth technologies in
detail, these articles are required reading as we cannot cover
everything here.
Selecting the Multi option from the main game menu (see Figure 4) displays the multiplayer menu shown in Figure 8.
In this game, if you choose Player1 you are the 'Master' (or game
server, if you like). If you choose Player2, you are the 'Slave'.
Before connections
can be established between Bluetooth devices, the Bluetooth stack must
be initialized, devices must be found (known as the inquiry phase) and
then the available services on each device must be queried (known as the
discovery phase).
In the inquiry phase, the
master device detects other devices in the area in order to determine
their types and Bluetooth addresses. The type of device is referred to
as the class of device (CoD). The CoD is simply a number that indicates
whether the remote device is a computer, an audio headset, or a mobile
phone. In JSR-82, this is represented by the DeviceClass class. You retrieve the CoD by calling the getMajorDeviceClass()
method, which returns an integer. These integers are well-known,
centrally assigned fixed numbers – for example, mobile phones have the
value 0x0200.
There are two modes commonly
used to find other devices – the General Inquiry Access Code (GIAC) and
the Limited Inquiry Access Code (LIAC). The Symbian OS Bluetooth stack
does not support the use of the LIAC so we won't discuss it any further
here.
The following code snippet shows how to start a search for nearby devices using JSR-82 classes:
// this throws a BluetoothStateException if Bluetooth is not enabled,
// which is a good way to check that it is before proceeding
LocalDevice localDevice = LocalDevice.getLocalDevice();
// start an inquiry for other devices in GIAC mode
DiscoveryAgent discoveryAgent = localDevice.getDiscoveryAgent();
discoveryAgent.startInquiry(DiscoveryAgent.GIAC, listener);
Notice the listener variable passed on the last line above. This is a reference to an object that implements the DiscoveryListener
interface which defines a handler interface for a series of callbacks
that occur during the inquiry and discovery phases. To stop an inquiry
in progress, you can also pass this handler to the cancelInquiry() method of the DiscoveryAgent class. For reference, this interface is shown below:
public interface DiscoveryListener
...
// called once for each device found during the inquiry phase
void deviceDiscovered(RemoteDevice btDevice, DeviceClass cod);
void inquiryCompleted(int discType);
// called during service discovery
void servicesDiscovered(int transID, ServiceRecord[] servRecord);
void serviceSearchCompleted(int transID, int respCode);
Once a set of devices matching
the desired CoD has been located, they need to be queried in order to
find out what services (identified using UUIDs) they offer. In the case
of a Bluetooth game, we are interested in the game 'service' that we
have built. Each device is queried in turn and the servicesDiscovered() callback method is called on the supplied listener, passing an array of ServiceRecord
objects. These represent the various attributes of services found on
the remote device. You can specify what attributes (such as, service
name) that you want returned in this process as part of the discovery
setup:
// an array of service ids we want to find
UUID[] uuid = new UUID[1];
RemoteDevice remoteDevice;
...
// include service name attribute in returned service records
int attribs[] = new int[]{0x0100};
...
discoveryAgent.searchServices(attribs, uuid, remoteDevice, listener);
The discovery process
callback methods are also supplied with a transaction identifier that
can be used to stop an active service discovery process by passing this
to the cancelServiceSearch() method of the DiscoveryAgent class.
Once devices and services
have been located and queried, we are ready to open a connection. As
already mentioned, setting up a Bluetooth connection can be a little
confusing at first because, unlike in other networking scenarios,
connections are initiated by the server rather than the client. To make
sure that our design remains clean, we've defined an observer interface
that is notified whenever our Bluetooth state changes:
public interface IBluetoothObserver {
public void notifyStateChange(int newState);
public void notifyMessageReceived(String message);
}
In Figure 8.5, note that the GameController class implements the IBluetoothObserver interface shown above. Using an observer is good practice as it keeps UI-related code separate from Bluetooth management code.
The listing below shows the states that are defined in the Bluetooth-Manager class for this game. You can see that each state change notifies the registered observer instance in the setState() method:
// states
public static final int IDLE = 0;
public static final int DETECTING_BLUETOOTH = 1;
public static final int BLUETOOTH_ENABLED = 2;
public static final int BLUETOOTH_DISABLED = 3;
public static final int FINDING_DEVICES = 4;
public static final int FINDING_SERVICES = 5;
public static final int WAITING = 6;
public static final int CONNECTING = 7;
public static final int CONNECTED = 8;
public static final int CONNECTION_FAILED = 9;
public static final int CANCELLING = 10;
public static final int DISCONNECTED = 11;
// called on state transitions
private void setState(int state){
this.state = state;
observer.notifyStateChange(state);
}
It is the slave (client)
that opens a server socket and waits for a connection request from the
master as illustrated in the code snippet below taken from the BluetoothManager class in our sample game:
String SERVICE_URL = "btspp://localhost:" + SERVICE_UUID;
private void startClientMode(){
...
// advertise that we have the game
String clientName = localDevice.getFriendlyName();
String url = SERVICE_URL + ";name=" + clientName;
...
notifier = (StreamConnectionNotifier) Connector.open(url);
// wait for server-initiated connection
connection = (StreamConnection) notifier.acceptAndOpen();
// get I/O streams
inputStream = connection.openInputStream();
outputStream = connection.openOutputStream();
...
// notify our Bluetooth observer that we are connected
setState(CONNECTED);
Since the master device has
already run an inquiry for devices and discovered their available
services, all that remains is for the master to initiate a connection to
the remote device (the slave). Bluetooth addresses are cumbersome and
change frequently so the master uses the getConnectionUrl() method of a remote device's ServiceRecord to determine how to open a connection to it:
private void startServerMode(){
...
ServiceRecord serviceRecord;
...
// specify connection settings
int security = ServiceRecord.NOAUTHENTICATE_NOENCRYPT;
boolean mustBeMaster = false;
// determine url to use given above
String url = serviceRecord.getConnectionURL(security, mustBeMaster);
// initiate the connection
connection = (StreamConnection) Connector.open(url);
// get I/O streams
inputStream = connection.openInputStream();
outputStream = connection.openOutputStream();
// notify our Bluetooth observer that we are connected
setState(CONNECTED);
Once connections have
been established you can use normal read and write methods to send data
between the connected handsets. In Finding Higgs,
we have kept data transfers minimal as the main point of them is simply
to keep each player aware of the other player's current score.
Of particular interest,
however, is that in this case we want the game on each handset to start
at the same time (as closely as possible) since it's only a one-minute
game and it's far less fun when your opponent finishes five seconds
before you do because they started five seconds earlier. To achieve
this, we wait until each device is connected, show the game screen
(which may be faster on more powerful hardware) and then
inform the other device that it is ready. Only when both devices have
received the initialization message do they start the game loop:
// called when Bluetooth connection is established
public void startNewGame() throws Exception {
...
controller.getDisplay().setCurrent(gameScreen);
notifyInitialised();
...
}
private void notifyInitialised(){
try{
gameStatus = GAME_STATUS_INITIALISED;
if(isMultiPlayerGame()){
bluetoothManager.sendMessage(MSG_GAME_INITIALISED);
}
else{
// single player game so just start
startGame();
}
}
...
}
// called whenever the Bluetooth connection receives a message
public void notifyMessageReceived(String message){
if(message.equals(MSG_GAME_INITIALISED)){
startGame();
}
else{ // remote player score updated
gameScreen.handleMessage(message);
}
}
Once everything has
been constructed and synchronized, we only exchange messages when either
player's score changes. In this case, it is our GameEngine instance that handles this:
// called from game thread – update local points and notify
// remote player when playing multi-player game
private void updateScore(){
try{
pointsP1 += POINTS_PER_ATTACKER;
if(gameController.isMultiPlayerGame()){
bluetoothManager.sendMessage(String.valueOf(pointsP1));
}
}
...
}
In a more complex game, it
would be advisable to define a message abstraction class instead of
just passing strings back and forth. For example, a common approach is
to use messages containing pre-defined integer codes (opcodes) and an
optional payload. However you do it, in the end your approach need only
match your requirements and you can be as complex or simplistic as you
need.