logo头像

学如逆水行舟

iOS简易蓝牙对战五子棋游戏设计思路之一——核心蓝牙通讯类的设计

iOS简易蓝牙对战五子棋游戏设计思路之一——核心蓝牙通讯类的设计

一、引言

本系列博客将系统的介绍一款蓝牙对战五子棋的开发思路与过程,其中的核心部分有两个,一部分是蓝牙通讯中对战双方信息交互框架的设计与开发,一部分是五子棋游戏中棋盘逻辑与胜负判定的算法实现。本篇博客将介绍游戏中蓝牙通讯类的设计思路

二、设计通讯类的核心想法

在前篇的一篇博客中,我们有详细的介绍iOS中蓝牙4.0技术的应用与系统框架CoorBluetooth.framework中提供的编程接口的用法。博客地址如下,如果读者需要更详细的了解iOS中蓝牙技术的使用,可以先阅读这篇博客:

iOS开发之蓝牙通讯:http://my.oschina.net/u/2340880/blog/548127

在使用蓝牙进行应用间通讯交互时,必须有一方作为中心设备,有一方作为外围设备。举一个简单的例子,通过手机蓝牙可以和刷卡设备、打印机等进行信息交互,这里的刷卡设备、打印机就充当着外围设备的角色,手机就充当着中心设备的角色。在中心设备与外围设备间,外设负责向周围广播广告告知其他设备自己的存在,中心设备接收到外设广播的广告后可以选择目标设备进行连接,当然,外设的广播的广告中会携带一些身份信息供中心设备进行识别。一旦中心设备与外设建立连接,中心设备变可以使用外设提供的服务,一个外设可以提供多个服务,例如一款蓝牙打印机外设可能会提供两种服务,一种服务向中心设备发送约定信息,告知中心设备支持的打印格式,一种服务获取中心设备的数据来进行打印服务。服务是中心设备与外设机型通讯的功能标识,然而具体的通讯媒介则是由服务中的特征值来完成的,一个服务也可以提供多个特征值。可以这样理解,特征值是两设备进行蓝牙通讯的最小通讯单元,是读写数据的载体。

上面简单介绍了在蓝牙通讯中的一些基本流程与相关概念,应用于游戏中略微有一些区别,首先我们这款游戏应该具备既可以作为中心设备也可以作为外设的能力,因此,我们需要将中心设备的通讯模式与外设的通讯模式都集成与游戏的通讯框架中。游戏的双方要建立连接应该有如下几个过程:

1.有一方建立游戏,作为房主。

2.由一方作为游戏的加入者,扫描附近的游戏。

3.外设提供的服务中应该至少有两个特征值,一个用于己方下子后通知对方设备,一个用于监听对方设备的下子操作。

由上面分析可知,游戏中的房主正是充当蓝牙通讯中的外设,它将广播广告告知周围设备自己的存在。而游戏中的加入者则是充当着蓝牙通讯中的中心设备,扫描到周围的游戏房间后进行连接加入,开始游戏。

三、游戏中蓝牙通讯类的设计

创建一个命名为BlueToothTool的工具类,作为游戏的蓝牙通讯类,编写其头文件如下:

BlueToothTool.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreBluetooth/CoreBluetooth.h>
//这个代理用于处理接收到对方设备发送来的数据后的回调
@protocol BlueToothToolDelegate <NSObject>
//获取对方数据
-(void)getData:(NSString *)data;
@end

@interface BlueToothTool : NSObject<CBPeripheralManagerDelegate,CBCentralManagerDelegate,CBPeripheralDelegate,UIAlertViewDelegate>
//代理
@property(nonatomic,weak)id<BlueToothToolDelegate>delegate;
//标记是否是游戏中的房主
@property(nonatomic,assign)BOOL isCentral;
/**
 *获取单例对象的方法
 */
+(instancetype)sharedManager;
/*
 *作为游戏的房主建立游戏房间
 */
-(void)setUpGame:(NSString *)name block:(void(^)(BOOL first))finish;
/*
 *作为游戏的加入者查找附近的游戏
 */
-(void)searchGame;
/**
 *断块连接
 */
-(void)disConnect;
/*
 *进行写数据操作
 */
-(void)writeData:(NSString *)data;
@end

实现BlueToothTool.m文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
#import "BlueToothTool.h"
@implementation BlueToothTool
{
    //外设管理中心
    CBPeripheralManager * _peripheralManager;
    //外设提供的服务
    CBMutableService * _ser;
    //服务提供的读特征值
    CBMutableCharacteristic * _readChara;
    //服务提供的写特征值
    CBMutableCharacteristic * _writeChara;
    //等待对方加入的提示视图
    UIView * _waitOtherView;
    //正在扫描附近游戏的提示视图
    UIView * _searchGameView;
    //设备中心管理对象
    CBCentralManager * _centerManger;
    //要连接的外设
    CBPeripheral * _peripheral;
    //要交互的外设属性
    CBCharacteristic * _centerReadChara;
    CBCharacteristic * _centerWriteChara;
    //开始游戏后的回调 告知先手与后手信息
    void(^block)(BOOL first);
}
//实现单例方法
+(instancetype)sharedManager{
    static BlueToothTool *tool = nil;
    static dispatch_once_t predicate;
    dispatch_once(&predicate, ^{
        tool = [[self alloc] init];
    });
    return tool;
}
//实现创建游戏的方法
-(void)setUpGame:(NSString *)name block:(void (^)(BOOL))finish{
    block = [finish copy];
    if (_peripheralManager==nil) {
        //初始化服务
         _ser= [[CBMutableService alloc]initWithType:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"] primary:YES];
        //初始化特征
        _readChara = [[CBMutableCharacteristic alloc]initWithType:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"] properties:CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable];
        _writeChara = [[CBMutableCharacteristic alloc]initWithType:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00068"] properties:CBCharacteristicPropertyWriteWithoutResponse value:nil permissions:CBAttributePermissionsWriteable];
        //向服务中添加特征
        _ser.characteristics = @[_readChara,_writeChara];
        _peripheralManager = [[CBPeripheralManager alloc]initWithDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    }
    //设置为房主
    _isCentral=YES;
    //开始广播广告
    [_peripheralManager startAdvertising:@{CBAdvertisementDataLocalNameKey:@"WUZIGame"}];
}
//外设检测蓝牙状态
-(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral{
    //判断是否可用
    if (peripheral.state==CBPeripheralManagerStatePoweredOn) {
        //添加服务
        [_peripheralManager addService:_ser];
        //开始广播广告
        [_peripheralManager startAdvertising:@{CBAdvertisementDataLocalNameKey:@"WUZIGame"}];
    }else{
        //弹提示框
        dispatch_async(dispatch_get_main_queue(), ^{
        [self showAlert];
        });
    }
}
//开始放广告的回调
-(void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral error:(NSError *)error{
    if (_waitOtherView==nil) {
        _waitOtherView = [[UIView alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 240, 200, 100)];
        dispatch_async(dispatch_get_main_queue(), ^{
        UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 200, 100)];
        label.backgroundColor = [UIColor clearColor];
        label.textAlignment = NSTextAlignmentCenter;
        label.text = @"等待附近玩家加入";
        [_waitOtherView addSubview:label];
        _waitOtherView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
            [[[UIApplication sharedApplication].delegate window]addSubview:_waitOtherView];
        });

    }else{
        dispatch_async(dispatch_get_main_queue(), ^{
            [_waitOtherView removeFromSuperview];
            [[[UIApplication sharedApplication].delegate window]addSubview:_waitOtherView];
        });
    }
}


//添加服务后回调的方法
-(void)peripheralManager:(CBPeripheralManager *)peripheral didAddService:(CBService *)service error:(NSError *)error{
    if (error) {
        NSLog(@"添加服务失败");
    }
    NSLog(@"添加服务成功");
}

//中心设备订阅特征值时回调
-(void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central didSubscribeToCharacteristic:(CBCharacteristic *)characteristic{
    [_peripheralManager stopAdvertising];
    if (_isCentral) {
        UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"" message:@"请选择先手后手" delegate:self cancelButtonTitle:@"我先手" otherButtonTitles:@"我后手", nil];
        dispatch_async(dispatch_get_main_queue(), ^{
            [_waitOtherView removeFromSuperview];
            [alert show];
        });
    }
}
//收到写消息后的回调
-(void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray<CBATTRequest *> *)requests{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.delegate getData:[[NSString alloc]initWithData:requests.firstObject.value encoding:NSUTF8StringEncoding]];
    });
}
//弹提示框的方法
-(void)showAlert{
    UIAlertView * alert = [[UIAlertView alloc]initWithTitle:@"温馨提示" message:@"请确保您的蓝牙可用" delegate:nil cancelButtonTitle:@"好的" otherButtonTitles:nil, nil];
    [alert show];
}
//===============================================================作为游戏加入这实现的方法========
//搜索周围游戏
-(void)searchGame{
    if (_centerManger==nil) {
        _centerManger = [[CBCentralManager alloc]initWithDelegate:self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
    }else{
        [_centerManger scanForPeripheralsWithServices:nil options:nil];
        if (_searchGameView==nil) {
            _searchGameView = [[UIView alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 240, 200, 100)];
            UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 200, 100)];
            label.backgroundColor = [UIColor clearColor];
            label.textAlignment = NSTextAlignmentCenter;
            label.text = @"正在扫加入描附近游戏";
            _searchGameView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
            [_searchGameView addSubview:label];
            [[[UIApplication sharedApplication].delegate window]addSubview:_searchGameView];
        }else{
            [_searchGameView removeFromSuperview];
            [[[UIApplication sharedApplication].delegate window]addSubview:_searchGameView];
        }
    }
    //设置为游戏加入方
    _isCentral = NO;
}
//设备硬件检测状态回调的方法 可用后开始扫描设备
-(void)centralManagerDidUpdateState:(CBCentralManager *)central{
    if (_centerManger.state==CBCentralManagerStatePoweredOn) {
        [_centerManger scanForPeripheralsWithServices:nil options:nil];
        if (_searchGameView==nil) {
             dispatch_async(dispatch_get_main_queue(), ^{
            _searchGameView = [[UIView alloc]initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 240, 200, 100)];
            UILabel * label = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, 200, 100)];
            label.backgroundColor = [UIColor clearColor];
            label.textAlignment = NSTextAlignmentCenter;
            label.text = @"正在扫加入描附近游戏";
            _searchGameView.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4];
            [_searchGameView addSubview:label];
                 [[[UIApplication sharedApplication].delegate window]addSubview:_searchGameView];
             });
        }else{
            
            dispatch_async(dispatch_get_main_queue(), ^{
                [_searchGameView removeFromSuperview];
                [[[UIApplication sharedApplication].delegate window]addSubview:_searchGameView];
            });
        }
    }else{
         dispatch_async(dispatch_get_main_queue(), ^{
        [self showAlert];
         });
    }
}
//发现外设后调用的方法
-(void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary<NSString *,id> *)advertisementData RSSI:(NSNumber *)RSSI{
    //获取设备的名称 或者广告中的相应字段来配对
    NSString * name = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey];
    if ([name isEqualToString:@"WUZIGame"]) {
        //保存此设备
        _peripheral = peripheral;
        //进行连接
        [_centerManger connectPeripheral:peripheral options:@{CBConnectPeripheralOptionNotifyOnConnectionKey:@YES}];
    }
}
//连接外设成功的回调
-(void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
    NSLog(@"连接成功");
    //设置代理与搜索外设中的服务
    [peripheral setDelegate:self];
    [peripheral discoverServices:nil];
     dispatch_async(dispatch_get_main_queue(), ^{
         [_searchGameView removeFromSuperview];
     });
}
//连接断开
-(void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error{
    NSLog(@"连接断开");
    [_centerManger connectPeripheral:peripheral options:@{CBConnectPeripheralOptionNotifyOnConnectionKey:@YES}];
}
//发现服务后回调的方法
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
    for (CBService *service in peripheral.services)
    {
        //发现服务 比较服务的UUID
        if ([service.UUID isEqual:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"]])
        {
            NSLog(@"Service found with UUID: %@", service.UUID);
            //查找服务中的特征值
            [peripheral discoverCharacteristics:nil forService:service];
            break;
        }
        
    }
}
//开发服务中的特征值后回调的方法
-(void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error{
    for (CBCharacteristic *characteristic in service.characteristics)
    {
        //发现特征 比较特征值得UUID 来获取所需要的
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00067"]]) {
            //保存特征值
            _centerReadChara = characteristic;
            //监听特征值
            [_peripheral setNotifyValue:YES forCharacteristic:_centerReadChara];
        }
        if ([characteristic.UUID isEqual:[CBUUID UUIDWithString:@"68753A44-4D6F-1226-9C60-0050E4C00068"]]) {
            //保存特征值
            _centerWriteChara = characteristic;
        }
    }
}
//所监听的特征值更新时回调的方法
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
    //更新接收到的数据
    NSLog(@"%@",[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]);
    //要在主线程中刷新
    dispatch_async(dispatch_get_main_queue(), ^{
        [self.delegate getData:[[NSString alloc]initWithData:characteristic.value encoding:NSUTF8StringEncoding]];
    });
}

-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    //告诉开发者先后手信息
    if (buttonIndex==0) {
        if (_isCentral) {
            block(1);
        }else{
            block(0);
        }
    }else{
        if (_isCentral) {
            block(0);
        }else{
            block(1);
        }
    }
}
//断开连接
-(void)disConnect{
    if (!_isCentral) {
        [_centerManger cancelPeripheralConnection:_peripheral];
      [_peripheral setNotifyValue:NO forCharacteristic:_centerReadChara];
    }
}
//写数据
-(void)writeData:(NSString *)data{
    if (_isCentral) {
        [_peripheralManager updateValue:[data dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:_readChara onSubscribedCentrals:nil];
    }else{
        [_peripheral writeValue:[data dataUsingEncoding:NSUTF8StringEncoding] forCharacteristic:_centerWriteChara type:CBCharacteristicWriteWithoutResponse];
    }
}

@end

附录:游戏的源码已经放在git上,时间比较仓促,只用了一下午来写,其中还有许多细节与bug没有进行调整,有需要的可以作为参考:

git地址:https://github.com/ZYHshao/BlueGame

专注技术,热爱生活,交流技术,也做朋友。

——珲少 QQ群:203317592