如何制作一个简单的蓝牙网络多人扑克游戏第一部分

翻译人:hany3000  博客:http://blog.csdn.net/hany3000

这篇文章作者是ios教程团队成员Matthijs Hollemans, 他是一位ios开发人员、设计师,你能通过Google+ 和 Twitter找到他。

在App Store里面,纸牌游戏非常流行-超过2500个程序以上,因此是时候raywenderlich.com 给你演示如何制作一个纸牌游戏了。 

这个教程7部分,将告诉你如何制作一个多人游戏,因此你跟你的朋友通过蓝牙或者 Wi-Fi 点对点的玩这个游戏。 

在这个教程中,你制作一个游戏,你不必使用OpenGL也不必使用游戏引擎像cocos2d,代替他们的事,你只是用UIImageView和UIView就可以实现他们。 

不使用OpenGL或者cocos2d的原因是,你真的不需要使用他们。 UIKit对于你在这的功能已经足够快了。像这样的单一纸牌游戏,屏幕上大多时候的物体都是静态的,你要做的仅仅是很多时候只是一个动画 .

跟着这个教程做,你需要使用xcode4.3以上的版本,如果你只有xcode4.2的话,你需要升级一下!!! 

为了测试这个多人游戏的功能,你需要至少两台运行ios5的机器或者更新的系统,如果你有一台 Wi-Fi 网络,你可以只使用一个机器测试,但是,比较理想的是你要有一台以上(我写这个教程做测试的时候使用四台不同的机器做测试) .

如果你是个新手的话,要先点击 相关教程介绍 学习下相关只是,哪里你能看到一个游戏视频,我们邀请你来做我们的特殊嘉宾。 

然后邀请你的朋友来玩这个纸牌游戏-你自己的多人纸牌游戏程序! 

介绍: Snap!

你要制作的游戏是一个小孩玩的游戏,叫 Snap!. 当我做完的时候,效果图如下 :

The finished game of Snap!

实际上,你跟snap规则不一样,游戏是由2到4个人玩使用52张牌,目标是赢得所有的牌,当你发出一对牌比较大的时候,你就赢牌了。 .

在每一轮的开始,洗牌机洗牌,然后按顺时针围着桌子发牌,直到发完牌,纸牌被放在每一个玩家的对面。 

玩家按顺时针出牌,如果该你出了,你从你的牌里面出一张牌(最上面的),当你看到别人出的牌跟你的牌是一对的时候,要尽快的喊“snap”,如果两张牌的数字是一样的时候,就是一对。 

看“snap”最快的玩家,就赢得了那些牌,并把牌添加到自己手里,知道一个玩家赢得了所有的纸牌。如果大家的牌都没有一对,而一个玩家喊了“snap”,那么他必须赔偿每个人一张牌。 

程序流

这是个多人游戏,使用蓝牙或者WiFi通讯,你将使用 Game Kit framework 库来制作这些,提示你仅仅是使用Game Kit链接特性中的点对点功能,这个程序将不使用游戏中心的功能,实际上,严格来说这个教程本身是从GameKIt摘出来的单独类 GKSession.

在这篇文章的第一部分,你将学会如何连接玩家的机器,以致他们能使用蓝牙或者WiFi交流,当然是Snap交流!通常来说必然有一个玩家做主机,其他玩家将加入这个最初玩家建立的会话,其他玩家都叫做客户端。  

程序的界面大概如下:

Mockup of the main screen

这个程序的主菜单在屏幕的上侧,第一个事情是玩家看到的,他能决定是做一个主机,来允许其他玩家加入,或者加入其他玩家建立的主机,或者同计算机玩单机游戏。 .

Mockup of the Host Game screen

主机游戏菜单包含一个视图列表。列出要加入这个会话的游戏玩家,按开始按钮将开始游戏,点了开始以后,就不能再接受新的玩家加入了,通常玩家决定在他们之间谁是主机,然后其他玩家都加入这个游戏。 

Mockup of the Join Game screen

菜单“加入游戏”屏幕跟主机游戏屏幕差不多,除了没有开始菜单意外,表视图中也列出了加入的玩家(这对多人玩家游戏界面看上去效果不错),这也提示你是加入到这个游戏中了,现在在等着主机玩家点开始。 .

Mockup of the game screen

游戏屏幕显示玩家围着一个桌子坐下,每个玩家都面对自己的牌,在屏幕的下侧,右下角有个按钮让本地玩家喊“Snap”(当你玩iphone游戏的时候,不是让你真正的喊出来),如果任何其他玩家喊“snap”的时候,你会看到在那个玩家旁边会出现一个说话的泡泡。 

开始

为了节省你的时间,我已经准备了xcode的基本项目,包括所有的资源(例如图片和nib文件),在第一部分,你需要这些资源文件,点击这里 下载资源文件,使用xocde打开Snap.xcodeproj.

如果你看资源文件,你将看到项目里包含一个单视图控制器,主视图控制器,运行程序,看上去像这:

The starter version of the app

主屏幕的logo纸牌包含5个UIImageViews控件(S, N, A, P 和大王) 和3个按钮 UIButtons. 你可以使用图片动画使得他们看上去更好看一些,但是首先你要改善这些按钮buttons.

下载的文件中包含一个文件名字叫Action_Man.ttf. 这是一个自定义字体,你将使用它来代替标准黑体Helvetica,或者iphone自带的字体,如果你在mac上点击  theAction_Man.ttf 你将在字体书中打开这个文件:

The Action Man font in Font Book

这个字体比标准字体看上去有点令人兴奋,如果你问我,不幸的是,你不能简单的把这个字体安装到mac上,然后在 Interface Builder里把它放在你的按钮上,你必须的写一些代码来完成这些,首先你需要告诉UIKit关于这个字体,以致于你能加载它。 

在xcode中打开文件Snap-Info.plist . 添加一个新的key为“Fonts provided by application.” 这是一个数组类型,给第一个item的名字为字体文件名字 Action_Man.ttf:

Adding font to Info plist

你也需要添加一个实际的ttf文件到项目中,拉这个文件到 Supporting Files 选项中:

The TTF file added to the project

提示你在添加目标弹出的对话框的时候,需要检查snap目标,否则字体文件爱你将不能包含进来。:

Adding the new file to the target

现在你可以在按钮和标签上使用设置这个字体了。 

UIFont *font = [UIFont fontWithName:@"Action Man" size:16.0f];
someLabel.font = font;

为了避免一次又一次的重复这个代码,我们为他建立一个类别,从File菜单里面,选择 New->File… option 选择“Objective-C category” 模板. 类别命名为 “SnapAdditions” 选择 “UIFont” 作为类别的类:

Creating a category on UIFont

这将建立两个新文件, UIFont+SnapAdditions.h 和 UIFont+SnapAdditions.m. 为了保持整洁好看,我放这两个文件到一个新组里,组名叫 Categories:

Placing the source files in the Categories group

在 UIFont+SnapAdditions.h 文件中替换下列代码:

@interface UIFont (SnapAdditions)
 
+ (id)rw_snapFontWithSize:(CGFloat)size;
 
@end

 .m 文件中替换一下代码:

#import "UIFont+SnapAdditions.h"
 
@implementation UIFont (SnapAdditions)
 
+ (id)rw_snapFontWithSize:(CGFloat)size
{
	return [UIFont fontWithName:@"Action Man" size:size];
}
 
@end

非常简单的类目,你已经添加了一个类的方法,  rw_snapFontWithSize: 这个方法将使用Actionman字体建立一个UIFont 对象 .

注意到那个字体的文件名字是Action_Man.ttf, 两个单词之间有个下划线,但是字体的名字没有下划线,你应该经常使用字体自己的名字,但是不是字体文件的名字,如果想找出字体的名字,从finder中双击它,使用字体书的软件打开(注意在字体书的屏幕点,它通常叫 Action Man而不是Action_Man.)

第二件事情就是注意,我在方法名字前加了前缀: “rw_”. 函数名字加前缀是一个好的方法,你能使用你的英文缩写(或者其他唯一标识符)来添加函数到标准库类中的类别, 就是确定函数的名字不要跟内在的函数冲突或者是一个Apple公司将来可能要命名的函数,apple公司不会添加一个函数命名为 “snapFontWithSize:”,但是还是安全一些比较好。

做了这些准备工作,你现在就能设置一个新的字体到视图控制器中的按钮上了,在MainViewController.m上侧,添加一个头文件, a 

#import "UIFont+SnapAdditions.h"

然后添加一个函数viewDidLoad 在@implementation 以内:

- (void)viewDidLoad
{
	[super viewDidLoad];
 
	self.hostGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
	self.joinGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
	self.singlePlayerGameButton.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
}

然后运行一次,按钮上的字将依新的字体显示。 :

The buttons with the new font

如果你仍然看到的是旧的字体,确认下Action_Man.ttf 文件是不是放在程序中,选择这个文件,确定在Target Membership 选项中的选择框是被选中的 (在xcode窗口右侧的文件检查器中):

Target membership checkbox

提示: 当你添加自定义字体到程序中的时候,你要小心的阅读字体许可,字体文件有版权的,如果你想添加字体到你的程序中的时候,通常需要付费,幸运的是这个 Action Man 字体是免费的使用和发布.

这个按钮上的字体看上去好看很多,但是正常的按钮都有一个边界,让我们来添加边界,你将设置一组拉伸的图片,这些图片已经被添加到项目中,命名为 Button.png和 ButtonPressed.png.

因为你已经有了几个不同的屏幕,这些屏幕都有类似的按钮,你将给自定义按钮放置代码到那个类别里。

添加一个新的类别到项目中,命名为 “SnapAdditions,” ,但是这一次呢,在UIButton上做一个类别,这就建立了两个新文件,UIButton+SnapAdditions.h 和 UIButton+SnapAdditions.m.把这些放到一个类别组里,然后在头文件中写入下列代码:

@interface UIButton (SnapAdditions)
 
- (void)rw_applySnapStyle;
 
@end

。m实现文件中放入如下代码:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"
 
@implementation UIButton (SnapAdditions)
 
- (void)rw_applySnapStyle
{
	self.titleLabel.font = [UIFont rw_snapFontWithSize:20.0f];
 
	UIImage *buttonImage = [[UIImage imageNamed:@"Button"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
	[self setBackgroundImage:buttonImage forState:UIControlStateNormal];
 
	UIImage *pressedImage = [[UIImage imageNamed:@"ButtonPressed"] stretchableImageWithLeftCapWidth:15 topCapHeight:0];
	[self setBackgroundImage:pressedImage forState:UIControlStateHighlighted];
}
 
@end

再来一次,仅仅有一个函数(有rw_做前缀),当你在一个UIButton对象中调用这个函数的时候, 它将给这个button一个新的背景图片,它也将设置字体。 .

MainViewController.m中为这个类添加一个头文件

#import "UIButton+SnapAdditions.h"

现在你能使用下列代码代替viewDidLoad

- (void)viewDidLoad
{
	[super viewDidLoad];
 
	[self.hostGameButton rw_applySnapStyle];
	[self.joinGameButton rw_applySnapStyle];
	[self.singlePlayerGameButton rw_applySnapStyle];
}

再次运行程序,现在按钮看上更真实些:

The buttons now have borders

动画介绍

现在你将让主屏幕有趣一点,那就在程序开始的时候把,logo的扑克牌飞进屏幕中?为了建立这个效果,添加几个方法到实现文件 MainViewController.m:

- (void)prepareForIntroAnimation
{
	self.sImageView.hidden = YES;
	self.nImageView.hidden = YES;
	self.aImageView.hidden = YES;
	self.pImageView.hidden = YES;
	self.jokerImageView.hidden = YES;
}
 
- (void)performIntroAnimation
{
	self.sImageView.hidden = NO;
	self.nImageView.hidden = NO;
	self.aImageView.hidden = NO;
	self.pImageView.hidden = NO;
	self.jokerImageView.hidden = NO;
 
	CGPoint point = CGPointMake(self.view.bounds.size.width / 2.0f, self.view.bounds.size.height * 2.0f);
 
	self.sImageView.center = point;
	self.nImageView.center = point;
	self.aImageView.center = point;
	self.pImageView.center = point;
	self.jokerImageView.center = point;
 
	[UIView animateWithDuration:0.65f
		delay:0.5f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = CGPointMake(80.0f, 108.0f);
			self.sImageView.transform = CGAffineTransformMakeRotation(-0.22f);
 
			self.nImageView.center = CGPointMake(160.0f, 93.0f);
			self.nImageView.transform = CGAffineTransformMakeRotation(-0.1f);
 
			self.aImageView.center = CGPointMake(240.0f, 88.0f);
 
			self.pImageView.center = CGPointMake(320.0f, 93.0f);
			self.pImageView.transform = CGAffineTransformMakeRotation(0.1f);
 
			self.jokerImageView.center = CGPointMake(400.0f, 108.0f);
			self.jokerImageView.transform = CGAffineTransformMakeRotation(0.22f);
		}
		completion:nil];
}

第一个函数,prepareForIntroAnimation, 简单的隐藏了5个UIImageViews,他们分别指向5个logo牌. 实际动画在performIntroAnimation中发生. 首先, 你把纸牌都放在屏幕外侧,跟中心水平,垂直于屏幕的底部下策,然后,你开始一个UIView 动画 这里放一个 UIImageViews 于最后动画停止的位置, 这样看上去他们是被从中间闪过去的。  

你将从viewWillAppear: 和 viewDidAppear调用这些函数:

- (void)viewWillAppear:(BOOL)animated
{
	[super viewWillAppear:animated];
 
	[self prepareForIntroAnimation];
}
 
- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];
 
	[self performIntroAnimation];
}

现在当你运行程序的时候,纸牌飞进屏幕然后翻出,非常的棒。 

Logo with fanned out cards

动画还不是很完美,不过当扑克牌飞到最终的位置上时候,如果那些按钮渐渐褪色在视图中显示,将会很不错,添加下列代码到prepareForIntroAnimationxiace下侧:

- (void)prepareForIntroAnimation
{
	. . .
 
	self.hostGameButton.alpha = 0.0f;
	self.joinGameButton.alpha = 0.0f;
	self.singlePlayerGameButton.alpha = 0.0f;
 
	_buttonsEnabled = NO;
}

这将使得这些按钮完全透明,添加uiview动画到这个函数的末尾performIntroAnimation:

- (void)performIntroAnimation
{
	. . .
 
	[UIView animateWithDuration:0.5f
		delay:1.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.hostGameButton.alpha = 1.0f;
			self.joinGameButton.alpha = 1.0f;
			self.singlePlayerGameButton.alpha = 1.0f;
		}
		completion:^(BOOL finished)
		{
			_buttonsEnabled = YES;
		}];
}

这里你简单的把按钮设置成不透明,但是 _buttonsEnabled 这个变量是什么呢? 当这些按钮渐变显示的时候,你不想用户按这些按钮,仅仅在这些动画完成以后这些按钮才可以被按使用.

你将使用_buttonsEnabled 变量来忽略按钮在这些按钮上,当动画仍旧发生的时候,现在添加这个新的变量在 @implementation 区域:

@implementation MainViewController
{
	BOOL _buttonsEnabled;
}

运行程序,检查这些动画,你会发现流畅了很多。 

游戏套件和多人游戏

游戏套件(GameKIt)是一个iOS sdk的标准框架,它的主要特性是在游戏中心(GameCenter)中使用它(在这个教程中你不会用到)和语音聊天,但是它也有点对点链接特性包括蓝牙连接,如果所有的设备都在一个本地wifi网络的话,游戏套件也能使用而代替蓝牙(也有一些规格来规定点对点对应因特网的,但是你必须自己写很多的代码,现在比较容易的还是用游戏中心)

游戏套件的点对点特性对多人游戏来说是非常棒的,玩家在同一个房间,用他们自己的设备,据说使用蓝牙的时候,玩家之间不能超过10米(或者说30尺)

因此点对点交流意味着什么?每一个设备参与游戏套件的网络回话就是一个名字叫做”点“,一个设备作为服务器分发一个参与设备服务,做一个客户端寻找服务器提供服务,或者同时都做为客户端和服务器,为了完成这些,游戏套件在视图背后使用 Bonjour technology, 但是你不需要直接使用Bonjour技术.

当使用蓝牙的时候,设备不需要被配对,方法是你需要配对一个蓝牙鼠标或者键盘在你的设备上,游戏套件简单的让客户端寻找服务器,一旦连接完成后,设备就能发送信息到这个网络上的各个设备。.

当选择蓝牙还是wifi网络的时候你没有选项,游戏套件给你做决定,蓝牙在模拟器中不支持,但是wifi网络支持。 

当为这个教程做开发和测试的时候,我发现很容易使用模拟和一个或者两个物理设备做测试,在本地wifi网络上也是,如果你想去使用蓝牙玩,你将需要至少两台物理设备,并且要把蓝牙都打开。

提示: 没有游戏套件,可能使用Bonjour和蓝牙也能交流,但是如果你要建立一个多人游戏,使用游戏套件就会很容易,你隐藏了所有的网络事情,而给了你一个简单的类使用叫GKSession,这将渐渐是一个游戏套件类,你将使用它在这个教程中,还有它的委托类:GKSessionDelegate).

如果你正在建立两个人的游戏使用蓝牙或者wifi网络,你能使用游戏套件的GKPeerPickerController来在两个设备之间建立连接,看上去像这样:

The GKPeerPickerController user interface

这个GKPeerPickerController 非常 容易使用,但是它的通信能力有限只能建立在两台设备之间。 Snap! 需要四个玩家,因此这个教程需要你自己写彼此配对的代码。.

主机游戏屏幕

在这节里,你将添加主机游戏屏幕到程序中,这个屏幕让一个玩家建立一个游戏回话使得其他玩家能够加入。当你完成的时候,它看上去如下图::

The Host Game screen

列表视图列出了已经连接到这个主机的玩家,开始按钮开始这个游戏。也有一个文本框来允许你去修改你玩家的名字(默认使用设备的名字).

添加一个新的视图控制器的子类UIViewController 到这个项目,命名为HostViewController,禁止“With XIB for user interface” 这个选项,下面使用之前准备好的nib文件抓们给主机游戏屏幕使用的,你能在目录“Snap/en.lproj” 下找到,拖动HostViewController.xib 文件到项目中.

这个nib文件看上去如下图:

The Host Game nib

大部分UI的组成都是跟属性和动作函数有关系,因此你需要添加他们到你的 HostViewController 类中,否则当你调用nib的时候,程序会崩溃。

HostViewController.m中, 添加下列代码到类的扩展中(在文件顶部):

@interface HostViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@property (nonatomic, weak) IBOutlet UIButton *startButton;
@end

注意你正在把IBOutlet 属性放在实现文件 .m 文件中,不是放在头文件 .h 中,这是一个LLVM编译的一个新特性,从Xcode4.2的版本以上,它保持你的头文件。h文件非常干净,以致你仅仅能暴漏其他对象需要看到的变量和函数。

当然你需要合成这些属性,添加下列代码在@implementation之后:

@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;
@synthesize startButton = _startButton;

提示: 传说在Xcode的下一个版本,当你看到这篇文章的时候你应该已经在使用了,你能略去@synthesize这个范围 但是如果你使用Xcode4.3,你仍然必须把它们放在里面。.

重置 shouldAutorotateToInterfaceOrientation: 以致它仅能支持横向:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}

在文件底部添加下列函数:

- (IBAction)startAction:(id)sender
{
}
 
- (IBAction)exitAction:(id)sender
{
}
 
#pragma mark - UITableViewDataSource
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 0;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

最后,放置@interface 在 HostViewController.h 中:

@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>

这就足够使得主屏幕基本工作,但是你需要在用户触摸正确的按钮的时候启动它,添加一个导入文件到MainViewController.h中:

#import "HostViewController.h"

然后使用下列代码放hostGameAction: 函数在MainViewController.m 中:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
 
		[self presentViewController:controller animated:NO completion:nil];
	}
}

如果你现在运行这个程序,你将看到触摸主机游戏按钮的时候,会打开主机游戏屏幕,它能正常运行,但是看上去不是很好,你显示这个新的视图控制器,因为你把动画属性关闭了. 对于这个新的屏幕来说不是很华润的打开。

这里这样一个动画显示的不是很好,你应该知道是主机游戏屏幕划过主屏幕,所以建立一个新的动画,添加下列代码到文件 MainViewController.m中:

- (void)performExitAnimationWithCompletionBlock:(void (^)(BOOL))block
{
	_buttonsEnabled = NO;
 
	[UIView animateWithDuration:0.3f
		delay:0.0f
		options:UIViewAnimationOptionCurveEaseOut
		animations:^
		{
			self.sImageView.center = self.aImageView.center;
			self.sImageView.transform = self.aImageView.transform;
 
			self.nImageView.center = self.aImageView.center;
			self.nImageView.transform = self.aImageView.transform;
 
			self.pImageView.center = self.aImageView.center;
			self.pImageView.transform = self.aImageView.transform;
 
			self.jokerImageView.center = self.aImageView.center;
			self.jokerImageView.transform = self.aImageView.transform;
		}
		completion:^(BOOL finished)
		{
			CGPoint point = CGPointMake(self.aImageView.center.x, self.view.frame.size.height * -2.0f);
 
			[UIView animateWithDuration:1.0f
				delay:0.0f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.sImageView.center = point;
					self.nImageView.center = point;
					self.aImageView.center = point;
					self.pImageView.center = point;
					self.jokerImageView.center = point;
				}
				completion:block];
 
			[UIView animateWithDuration:0.3f
				delay:0.3f
				options:UIViewAnimationOptionCurveEaseOut
				animations:^
				{
					self.hostGameButton.alpha = 0.0f;
					self.joinGameButton.alpha = 0.0f;
					self.singlePlayerGameButton.alpha = 0.0f;
				}
				completion:nil];
		}];
}

提示: 当你添加这个函数的时候不是很麻烦(只要它位于@implementation 和 @end 之间),之前你必须生命这个函数在。h文件中,然后还要把它的签名放在实现文件的顶部或者你要确定你调用的任何函数都是在源文件上面,对于Xcode4.3的版本自带的llvm编译器来说就必要做这些了,这个编译器现在足够聪明的找到这些函数,你不用把它放在源文件中,如果你之前没有声明它的话。 

performExitAnimationWithCompletionBlock: 中运行的动画:把logo纸牌移走,然后同时把这些按钮渐渐消失,当动画结束的时候,它就执行之前在block中一个参数的函数中的代码。 

现在来改变 hostGameAction: 如下

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{	
			HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
 
			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

几乎和之前的相同,但是建立和显示主机屏幕的逻辑是在block中被打包的,当退出这个动画完成的时候会被执行。运行程序,你把动画代码放在一个分开的函数里以致你能按不同的按钮就能执行它。 

正如你在nib中看到的,这个主机屏幕用默认的字体,它的开始按钮没有边界,这很容易修复它,添加两个头文件到HostViewController.m:

#import "UIButton+SnapAdditions.h"
#import "UIFont+SnapAdditions.h"

添加下列代码代替 viewDidLoad:

- (void)viewDidLoad
{
	[super viewDidLoad];
 
	self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];;
	self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];
 
	[self.startButton rw_applySnapStyle];
}

因为你已经建立了这些方便的类目到你的字体和按钮风格,就是snap(哈哈)来确定屏幕看上去很友好,运行程序看下效果。.

有一些地方需要调整,这个屏幕有一个UITextField来允许玩家来输入它的名字,当你按这个文本的区域的时候,在屏幕上会打开键盘,这个键盘占用这个屏幕几乎一般的地方, 目前没办法避免这个。.

The on-screen keyboard

第一个方法来关闭键盘是一个大的蓝色完成按钮,现在触摸它什么都不会发生,添加下列代码到HostViewController.m 将解决这个:

#pragma mark - UITextFieldDelegate
 
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
	[textField resignFirstResponder];
	return NO;
}

第二个方法是在viewDidLoad中添加下列代码:

- (void)viewDidLoad
{
	. . .
 
	UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
	gestureRecognizer.cancelsTouchesInView = NO;
	[self.view addGestureRecognizer:gestureRecognizer];
}

你正在建立一个手势识别来回应简单的触屏,添加它到主视图中,现在当用户触屏到文本区域的时候,手势识别会发送“resignFirstResponder” 消息到文本区域,这将使得键盘消失。

注意你需要设置cancelsTouchesInView 参数为NO,否则,在屏幕上将不能触摸到任何东西,例如表视图和按钮。 .

”退出主机游戏“界面

这些按钮还没有什么功能,但是如果当按按钮左下角的时候能返回主屏幕,那将非常的好.

这个按钮连接着退出动作,现在这个还是空的,它应该关闭界面,你将使用委托协议来实现它,在这个程序里有好几个试图控制器,他们将通过委托来通信达到最小的依赖关系。添加下列代码到HostViewController.h, 在@interface 这一行得上侧:

@class HostViewController;
 
@protocol HostViewControllerDelegate <NSObject>
 
- (void)hostViewControllerDidCancel:(HostViewController *)controller;
 
@end

在@interface 区域内添加一个属性:

@property (nonatomic, weak) id <HostViewControllerDelegate> delegate;

属性需要被合成,因此在HostViewController.m, 添加如下:

@synthesize delegate = _delegate;

最后替代exitAction: 使用下列代码:

- (IBAction)exitAction:(id)sender
{
	[self.delegate hostViewControllerDidCancel:self];
}

这个思路很明确了: 你已经为主机视图控制器HostViewController声明了一个委托协议,当退出按钮被按下的时候,这个视图会告诉它的委托,这个主机游戏屏幕将会被关闭,这个委托就是负责关闭这个屏幕界面。

这里,当然这个委托的角色就是在主视图控制器MainViewController中被运行,修改下列代码在头文件 MainViewController.h中:

@interface MainViewController : UIViewController <HostViewControllerDelegate>

在 MainViewController.m, 把 hostGameAction: 函数修改为:

- (IBAction)hostGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{	
			HostViewController *controller = [[HostViewController alloc] initWithNibName:@"HostViewController" bundle:nil];
			controller.delegate = self;
 
			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

现在你正在建立MainViewController 中HostViewController的委托,最后,添加这个委托实现函数到MainViewController.m中:

#pragma mark - HostViewControllerDelegate
 
- (void)hostViewControllerDidCancel:(HostViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

没有一个动画就简单的关闭主机试图控制器屏幕,因为主视图的MainViewController’sviewWillAppear 将会被调用,飞动的纸牌动画将会出现,运行一次看下效果。 .

提示: 为了调试,当屏幕关闭的时候,我喜欢确定我的视图控制器是否被释放,因此我通常添加一个释放函数在我的视图控制器里,输出一个信息来记录是否释放。 :

- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}

即使这个项目使用ARC,它仍旧有可能内存泄露, ARC大大简化了内存管理,但是它不能释放调用“retain"周期(或者ownership周期) . 如果你有两个对象有彼此的strong 指针,他们永远都会驻留在内存中,这就是当我的对象被释放的时候我喜欢log一下的原因,保留一个眼睛在某些事情上。 

现在这个主机游戏界面完成了,在你开始游戏套件对话之前,先广播一下你的服务,你必须建立加入游戏界面,否则其他的设备就没有办法发现这个新的游戏套件对话。

“加入游戏”界面

这个界面跟主机游戏界面很类似,但是但是连接的两个类有很大不同(而不是说是HostViewController的重写类或者子类).但是因为跟之前做的很类似,所以你能很快的使用.

添加一个UIViewController 的子类到项目中,命名为JoinViewController. 禁止XIB 选项,我已经在代码中给你提供了一个nib,把它拉进项目中((在目录 “Snap/en.lproj/”下) .

oinViewController.h 使用下列代码代替:

@class JoinViewController;
 
@protocol JoinViewControllerDelegate <NSObject>
 
- (void)joinViewControllerDidCancel:(JoinViewController *)controller;
 
@end
 
@interface JoinViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>
 
@property (nonatomic, weak) id <JoinViewControllerDelegate> delegate;
 
@end

这跟你之前做的主机游戏界面很类似,替换下列代码在JoinViewController.m 中:

#import "JoinViewController.h"
#import "UIFont+SnapAdditions.h"
 
@interface JoinViewController ()
@property (nonatomic, weak) IBOutlet UILabel *headingLabel;
@property (nonatomic, weak) IBOutlet UILabel *nameLabel;
@property (nonatomic, weak) IBOutlet UITextField *nameTextField;
@property (nonatomic, weak) IBOutlet UILabel *statusLabel;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
 
@property (nonatomic, strong) IBOutlet UIView *waitView;
@property (nonatomic, weak) IBOutlet UILabel *waitLabel;
@end
 
@implementation JoinViewController
 
@synthesize delegate = _delegate;
 
@synthesize headingLabel = _headingLabel;
@synthesize nameLabel = _nameLabel;
@synthesize nameTextField = _nameTextField;
@synthesize statusLabel = _statusLabel;
@synthesize tableView = _tableView;
 
@synthesize waitView = _waitView;
@synthesize waitLabel = _waitLabel;
 
- (void)dealloc
{
	#ifdef DEBUG
	NSLog(@"dealloc %@", self);
	#endif
}
 
- (void)viewDidLoad
{
	[super viewDidLoad];
 
	self.headingLabel.font = [UIFont rw_snapFontWithSize:24.0f];
	self.nameLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.statusLabel.font = [UIFont rw_snapFontWithSize:16.0f];
	self.waitLabel.font = [UIFont rw_snapFontWithSize:18.0f];
	self.nameTextField.font = [UIFont rw_snapFontWithSize:20.0f];
 
	UITapGestureRecognizer *gestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self.nameTextField action:@selector(resignFirstResponder)];
	gestureRecognizer.cancelsTouchesInView = NO;
	[self.view addGestureRecognizer:gestureRecognizer];
}
 
- (void)viewDidUnload
{
	[super viewDidUnload];
	self.waitView = nil;
}
 
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
	return UIInterfaceOrientationIsLandscape(interfaceOrientation);
}
 
- (IBAction)exitAction:(id)sender
{
	[self.delegate joinViewControllerDidCancel:self];
}
 
#pragma mark - UITableViewDataSource
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	return 0;
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}
 
#pragma mark - UITextFieldDelegate
 
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
	[textField resignFirstResponder];
	return NO;
}
 
@end

这没什么新内容,除了这个 waitView 开关变量,注意这个属性像其他属性一样,声明为 “strong,” 代替“weak” . 因为在nib中它是最上层的视图,这就完成了

The Join Game nib

这里标为strong是很重要的以致它有个代理指向,避免它被回收,在第一个视图中你不必这么做是因为它在试图控制器中建立self。view属性的时候被retain了, 

在表视图中用户触按了主机名字hi后,你将放第二个视图(这个将显示“Connecting…”在主视图的顶部,你可以使用一个新的视图控制器 这很容易 

增强 MainViewController.h:

#import "HostViewController.h"
#import "JoinViewController.h"
 
@interface MainViewController : UIViewController <HostViewControllerDelegate, JoinViewControllerDelegate>
 
@end

In MainViewController.m, replace joinGameAction: with:

- (IBAction)joinGameAction:(id)sender
{
	if (_buttonsEnabled)
	{
		[self performExitAnimationWithCompletionBlock:^(BOOL finished)
		{
			JoinViewController *controller = [[JoinViewController alloc] initWithNibName:@"JoinViewController" bundle:nil];
			controller.delegate = self;
 
			[self presentViewController:controller animated:NO completion:nil];
		}];
	}
}

然后添加一个实现委托函数

#pragma mark - JoinViewControllerDelegate
 
- (void)joinViewControllerDidCancel:(JoinViewController *)controller
{
	[self dismissViewControllerAnimated:NO completion:nil];
}

现在你有一个可以运行的加入游戏界面,最后加入逻辑匹配代码。 !

提示: 当写多人游戏的时候(或者其他网络软件),你最基本的要有一个两个架构选择:cs和p2p,即使你在游戏套件中使用p2p的API,这个游戏使用的是cs模式,使用主机游戏的这个人匹配服务器,其他玩家是客户端。 

Client-server vs peer-to-peer

在cs配置的时候。服务器是负责每一件事情,决定真实的事情,客户端发送他们的最新状态到服务器,然后服务器刷新他们的所有的客户端,但是客户端之间不能通讯,但是在p2p游戏中,所有的参与者是平等的。所有的点都做同样的工作,但是你需要关心每一个点都做同样的事情,没有拥有决定权的中心。 .

在这篇文章中,我们仍旧使用cs结构,建立游戏的这个点将是服务器。 

匹配

现在你已经有了可以运行的基本的主机游戏和加入游戏界面,你就可以添加匹配逻辑了,当你一个玩家按下主机游戏按钮的时候,它的设备应该广播Snap可以使用的服务!当一个玩家点击加入游戏界面的时候,它的设备就开始寻找任何标明snap可以使用的服务的设备。 .

游戏套件的GKSession 类对此做了大量的工作,但是你仍旧需要自己做很多事情, 而不是把所有的逻辑都放在视图控制器中。你将创建两个新类, MatchmakingServer 和 MatchmakingClient.如果试图控制器负责太多的事情的话,源代码会变得很麻烦,因此这就是为什么会建立两个新的对象来设置设备之间的连接。 .

在你开始建立这些新类之前,首先是添加游戏套见到游戏中,在 Target Summary 界面中,在  Libraries的Frameworks 下, 按下+ 按钮然后从列表中选择 GameKit.framework,把它添加到项目中。

Linking with GameKit framework

而不是把游戏套件的头文件都使用#import放在每一个需要游戏套件的头文件中,我倾向于把框架导入到预编译头文件中,打开Snap-Prefix.pch (在Supporting Files下),添加下列代码到#ifdef __OBJC__ section:

	#import <GameKit/GameKit.h>

现在游戏套件头将立刻应用与任何的源文件。 

还有一件事情你需要去做,就是添加一个特别的符号到你的Info.plist 文件中,来标明这个程序需要点对点的功能,因为不是所有的设备(特别是iPhone和iPod touch)都支持点对点.

打开 Snap-Info.plist 在“Required device capabilities” 下添加一个新的行,设值为 “peer-peer”:

Adding peer-peer to required device capabilities

类MatchmakingServer

添加一个新的Objective-C 类到项目中,设置为NSObject的子类,命名为MatchmakingServer. 我建议把它放在一个新的组里,命名为“Networking.” ,替换 MatchmakingServer.h 中的内容如下:

@interface MatchmakingServer : NSObject <GKSessionDelegate>
 
@property (nonatomic, assign) int maxClients;
@property (nonatomic, strong, readonly) NSArray *connectedClients;
@property (nonatomic, strong, readonly) GKSession *session;
 
- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID;
 
@end

这个服务器有一个客户端连接列表,有一个变量来限制在同一时间最多有多少个客户端可以连接. Snap! 最多可以有四个玩家,因此,你不能允许三个以上的玩家来连接服务器 (服务器自己也算一个玩家) 

服务器也有GKSession 对象,它将负责各个设备之间的网络通讯,它也符合 GKSessionDelegate 协议,因为GKSession使得它知道很多重要的事件。 

目前, MatchmakingServer 只有一个函数,开始广播服务,接受来自客户端的连接,不久你将添加更多的功能。 .

在 MatchmakingServer.m替换下列代码:

#import "MatchmakingServer.h"
 
@implementation MatchmakingServer
{
	NSMutableArray *_connectedClients;
}
 
@synthesize maxClients = _maxClients;
@synthesize session = _session;
 
- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
	_connectedClients = [NSMutableArray arrayWithCapacity:self.maxClients];
 
	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
	_session.delegate = self;
	_session.available = YES;
}
 
- (NSArray *)connectedClients
{
	return _connectedClients;
}
 
#pragma mark - GKSessionDelegate
 
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state);
	#endif
}
 
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: connection request from peer %@", peerID);
	#endif
}
 
- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: connection with peer %@ failed %@", peerID, error);
	#endif
}
 
- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: session failed %@", error);
	#endif
}
 
@end

这是大多的样板的东西,这个 GKSessionDelegate到目前位置啥也没做,只是log下Xcode调试面板的结果,一个有趣的东西出现在 startAcceptingConnectionsForSessionID:

	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeServer];
	_session.delegate = self;
	_session.available = YES;

这里是你建立 GKSession 对象的地方,告诉它服务器的操作模式,这个方法将仅仅广播这个服务已经准备好了, 命名 sessionID 参数 – 但是它将不去寻找其他提供相同服务的设备,然后你告诉这个对话 session ,MatchmakingServer 是他的委托,设置 “available” 属性为真值YES, 这个参数是启动广播的,然后这就是你所有需要做的来获取一个游戏套件回话Game Kit session.

现在你将把 MatchmakingServer 放到主机游戏界面里,添加一个头文件到HostViewController.h中:

#import "MatchmakingServer.h"

作为一个变量实例添加一个MatchmakingServer 对象到视图控制器中HostViewController.m:

@implementation HostViewController
{
	MatchmakingServer *_matchmakingServer;
}

再添加一个函数到HostViewController.m:

- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];
 
	if (_matchmakingServer == nil)
	{
		_matchmakingServer = [[MatchmakingServer alloc] init];
		_matchmakingServer.maxClients = 3;
		[_matchmakingServer startAcceptingConnectionsForSessionID:SESSION_ID];
 
		self.nameTextField.placeholder = _matchmakingServer.session.displayName;
		[self.tableView reloadData];
	}
}

这就建立了MatchmakingServer 对象当主机游戏界面出现的时候,然后告诉它开始接受客户端的连接,它也放置设备的名字( 来自session.displayName) 把占位符放在 “Your Name:”文本区域. 如果玩家不输入它自己的名字,它将使用默认的占位符来被识别身份。 

这个新的代码直到你定义了SESSION_ID 符号才能正常工作,它不关心ID是什么,服务器和客户端都在同样的取值上都同意,在界面背后,游戏套件将把 这个设置成唯一标示符,因为服务器和客户端都需要这个符号,你将把它简单的添加到 prefix 文件. 打开 Snap-Prefix.pch 拷贝下列代码到底部 :

// The name of the GameKit session.
#define SESSION_ID @"Snap!"

运行代码,点击主机游戏按钮,如果你在虚拟器上运行,你会看到下列界面 

Host Game on Simulator

来自 GKSession的displayName属性包含一个字符串像这样 “com.hollance.Snap355561232…”, 等等,如果你在你实体设备上运行这个程序,它将显示 “Joe’s iPhone” 或者其他你第一次设置你的设备的时候起的名字.

你有了一个可以运行的游戏套件服务器可以广播”Snap!“服务,但是还没有客户端来连接它,现在你将来改变它,建立一个客户端的类 

类MatchingmakingClient

添加一个新的Objective-C 类到项目中,基类是NSObject,命名为MatchmakingClient, 把它放在Networking group. 写入下列代码到 MatchmakingClient.h:

@interface MatchmakingClient : NSObject <GKSessionDelegate>
 
@property (nonatomic, strong, readonly) NSArray *availableServers;
@property (nonatomic, strong, readonly) GKSession *session;
 
- (void)startSearchingForServersWithSessionID:(NSString *)sessionID;
 
@end

这就是MatchmakingServer做的镜像,代替客户端的连接,它有有空格服务器列表,替换下列代码。 :

#import "MatchmakingClient.h"
 
@implementation MatchmakingClient
{
	NSMutableArray *_availableServers;
}
 
@synthesize session = _session;
 
- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
	_availableServers = [NSMutableArray arrayWithCapacity:10];
 
	_session = [[GKSession alloc] initWithSessionID:sessionID displayName:nil sessionMode:GKSessionModeClient];
	_session.delegate = self;
	_session.available = YES;
}
 
- (NSArray *)availableServers
{
	return _availableServers;
}
 
#pragma mark - GKSessionDelegate
 
- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: peer %@ changed state %d", peerID, state);
	#endif
}
 
- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: connection request from peer %@", peerID);
	#endif
}
 
- (void)session:(GKSession *)session connectionWithPeerFailed:(NSString *)peerID withError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: connection with peer %@ failed %@", peerID, error);
	#endif
}
 
- (void)session:(GKSession *)session didFailWithError:(NSError *)error
{
	#ifdef DEBUG
	NSLog(@"MatchmakingClient: session failed %@", error);
	#endif
}
 
@end

这是这个类的准系统版本,你将慢慢充实它,提示你现在已经建立了一个GKSession对象在 GKSessionModeClient, 因为它将寻找可以连接的服务器(它自己不广播服务) .

现在 整合这个新的类到JoinViewController中,以致你可以使用客户端连接服务器,首先添加一个头文件到JoinViewController.h:

#import "MatchmakingClient.h"

然后添加一个变量实例到 JoinViewController.m:

@implementation JoinViewController
{
	MatchmakingClient *_matchmakingClient;
}

viewDidAppear中设置使得整个事件开始

- (void)viewDidAppear:(BOOL)animated
{
	[super viewDidAppear:animated];
 
	if (_matchmakingClient == nil)
	{
		_matchmakingClient = [[MatchmakingClient alloc] init];
		[_matchmakingClient startSearchingForServersWithSessionID:SESSION_ID];
 
		self.nameTextField.placeholder = _matchmakingClient.session.displayName;
		[self.tableView reloadData];
	}
}

到这里,你就可以测试了,确信你有两台设备,都可以使用蓝牙功能,或者把你的设备放在一个Wi-Fi网络,运行程序在你的设备和虚拟器中,其中的一个按击主机游戏。另外一个按击加入游戏, 

这个程序不显示任何东西在屏幕上,但是你应该可以在Xcode的调试面板里面到一组信息来自游戏套件自己 

服务器现实的信息大概如下: 

Snap[3810:707] BTM: attaching to BTServer
Snap[3810:707] BTM: posting notification BluetoothAvailabilityChangedNotification
Snap[3810:707] BTM: received BT_LOCAL_DEVICE_CONNECTABILITY_CHANGED event
Snap[3810:707] BTM: posting notification BluetoothConnectabilityChangedNotification

这些信息来自游戏套件自己,客户端也会显示这些来自游戏套件的信息,但是它现实的应该是: 

Snap[94530:1bb03] MatchmakingClient: peer 663723729 changed state 0

这个信息来自于GKSessionDelegate 的函数 session:peer:didChangeState:,它位于你的 MatchmakingClient 类中. 它告诉我们有一个设备ID是 “663723729″ 是可以对话了,换言之,这个客户端已经已经检测到服务器的存在了。 

提示: 这个是设备ID是一个游戏套件为了识别的不同的设别而使用的内部ID,它只是单独在对话中,每一次你运行这个程序,你的点都将获取不同的ID,你之后将使用这个ID。  

如果你有两个或者更多个设备,作为一个服务器你能获得更多的功能,这个教程的目标 来说,一个客户端只能连接服务器中的一个,但是它能检测出所有的服务器。试试把!!!!! 

请关注下一个教程!! 

展开阅读全文

没有更多推荐了,返回首页