Today Bluetooth Low Energy can be found in many cool applications, it can be used from simple data exchange to payment terminals and the more popular usage with iBeacons. But what if we want to build something funny with it? Like some simple game not even realtime, it may be even turn based game. Imagine you do not need to go through this long setup, waiting for server players to be ready etc.
Everyone knows that building good multiplayer game is hard, multiplayer itself is hard… But here I want to show you my small proof of concept of working bluetooth low enery multiplayer game.
It can be used in any kind of game! Strategy, board, rpg, race. I built§ a small demo project to show this in details but now let’s focus on basics:
Pros:
It’s simple!
Works with any device
No need to pair, login etc. Just come near other phone
Cons:
Bandwith (approx 30bytes of data per packet which todays is nothing)
Limited distance (work well in approx 20m range)
We have our interface class that will be used to extend functionality on both server and client logic (we use central and peripheral mode of our phone)
As you can see, it’s very simple - one send and one receive method as delegate. As both recive and send arguments, we can get the command used in your game to recognize packet type and data which will come along with this command.
Now we need to implement our server and client logic, i don’t want to describe in details how to setup BluetoothLE on iPhone so insted I will highlight only important methods like receiving and sending packet on both client and server side.
classKWSBluetoothLEClient:KWSBluetoothLEInterface,CBPeripheralManagerDelegate{overridefuncsendCommand(commandcommand:KWSPacketType,data:NSData?){if!interfaceConnected{return}varheader:Int8=command.rawValueletdataToSend:NSMutableData=NSMutableData(bytes:&header,length:sizeof(Int8))ifletdata=data{dataToSend.appendData(data)}ifdataToSend.length>kKWSMaxPacketSize{print("Error data packet to long!")return}self.peripheralManager.updateValue(dataToSend,forCharacteristic:self.readCharacteristic,onSubscribedCentrals:nil)}funcperipheralManager(peripheral:CBPeripheralManager,didReceiveWriteRequestsrequests:[CBATTRequest]){ifrequests.count==0{return;}forreqinrequestsas[CBATTRequest]{letdata:NSData=req.value!letheader:NSData=data.subdataWithRange(NSMakeRange(0,sizeof(Int8)))letremainingVal=data.length-sizeof(Int8)varbody:NSData?=nilifremainingVal>0{body=data.subdataWithRange(NSMakeRange(sizeof(Int8),remainingVal))}letactionValue:UnsafePointer<Int8>=UnsafePointer<Int8>(header.bytes)letaction:KWSPacketType=KWSPacketType(rawValue:actionValue.memory)!self.delegate?.interfaceDidUpdate(interface:self,command:action,data:body)self.peripheralManager.respondToRequest(req,withResult:CBATTError.Success)}}}
classKWSBluetoothLEServer:KWSBluetoothLEInterface,CBCentralManagerDelegate,CBPeripheralDelegate{overridefuncsendCommand(commandcommand:KWSPacketType,data:NSData?){if!interfaceConnected{return}varheader:Int8=command.rawValueletdataToSend:NSMutableData=NSMutableData(bytes:&header,length:sizeof(Int8))ifletdata=data{dataToSend.appendData(data)}ifdataToSend.length>kKWSMaxPacketSize{print("Error data packet to long!")return}ifletdiscoveredPeripheral=self.discoveredPeripheral{discoveredPeripheral.writeValue(dataToSend,forCharacteristic:self.writeCharacteristic,type:.WithResponse)}}funcperipheral(peripheral:CBPeripheral,didUpdateValueForCharacteristiccharacteristic:CBCharacteristic,error:NSError?){ifleterror=error{print("didUpdateValueForCharacteristic error: \(error.localizedDescription)")return}letdata:NSData=characteristic.value!letheader:NSData=data.subdataWithRange(NSMakeRange(0,sizeof(Int8)))letremainingVal=data.length-sizeof(Int8)varbody:NSData?=nilifremainingVal>0{body=data.subdataWithRange(NSMakeRange(sizeof(Int8),remainingVal))}letactionValue:UnsafePointer<Int8>=UnsafePointer<Int8>(header.bytes)letaction:KWSPacketType=KWSPacketType(rawValue:actionValue.memory)!self.delegate?.interfaceDidUpdate(interface:self,command:action,data:body)}}
In both cases sending and reciving is the same:
Sending:
Take raw value of the command
Save into NSData
Append using additional data that comes with the command
Send to peripheral/central
Receive:
Take NSData from central / peripheral (update request status if needed)
Get first byte to recognize command type
Take subset of Data by removing 1st byte and store it as value coming along with command
Take value of header byte and cast it to our PacketType
Send it to delegate
Thanks to that we can build our game logic like this:
//player is dead notify other playerself.communicationInterface!.sendCommand(command:.GameEnd,data:nil)//send some basic data about your player state (life, position)letcurrentPlayer=self.gameScene.selectedPlayervarpacket=syncPacket()packet.healt=currentPlayer!.healtpacket.posx=Float16CompressorCompress(Float32(currentPlayer!.position.x))letpacketData=NSData(bytes:&packet,length:sizeof(syncPacket))self.communicationInterface!.sendCommand(command:.HearBeat,data:packetData)//send some other info letdirectionData=NSData(bytes:¤tPlayer!.movingLeft,length:sizeof(Bool))self.communicationInterface!.sendCommand(command:.MoveDown,data:directionData)
Game works smoothly, there are no lags in connection and you can play almost instantly! And of course it allows you to integrate mutliplayer in your game in few minutes.
If you are starting your journey with gamedev or iOS and plan to build simple SpriteKit game with some basic multiplayer support it may be worth considering this option.
Demo project used to present the mechanics is available as always on github
Game require at least two iPhone 5 to test and play. To start simply open game, one of the players choose server, other one client mode and bring your phone next to each another. Once you do that you should be notified about successfull connection by tone sound.