zoukankan      html  css  js  c++  java
  • 使用UIKit制作卡牌游戏(一)ios游戏篇

    转自朋友Tommy 的翻译,自己只翻译了第三篇教程。

    译者: Tommy | 原文作者: Matthijs Hollemans写于2012/06/29 
    原文地址: http://www.raywenderlich.com/12735/how-to-make-a-simple-playing-card-game-with-multiplayer-and-bluetooth-part-1


    image

    这篇文章是由iOS教程团队成员Matthijs Hollemans发表的,一个经验丰富的开发工程师和设计师。你可以在Google+Twitter上找到他。

    纸牌游戏在App Store上是非常流行的,超过2500个app了并且还在持续增长,所以是时候由raywenderlich.com来来教大家做纸牌游戏了。

    另外,该系列教程总共7篇,用来展示如何做一个多人纸牌游戏,你可以和你的小伙伴们可以通过GameKit的peer-to-peer的特性使用蓝牙或Wi-Fi来玩。

    虽然说你用这篇教程来做的是个游戏,但你并不需要使用OpenGL或像Cocos2D一样的游戏框架,使用的仅仅是一些标准的UIImageView和一些UIView的基本动画。

    不使用OpenGL和Cocos2D的原因是我们不需要!对于做这个,UIKit足够了,并且UIKit擅长用于纸牌和棋盘游戏的制作,这类游戏的内容大都在屏幕內,并且只需要对一些view做一些简单的动画就可以了。

    要一步一步地学习下面的教程,你需要使用4.3或更高版本的Xcode,如何你现在使用的是4.2,那么是时候升级了!

    还有,要测试多用户的功能,你至少需要两台运行5.0或者更高版本系统的手机。如果你有Wi-Fi网络环境,你也可以用一个设备来玩,但最好还是用多个(我在写这边教程的时候用四个不同的设备)。

    继续下面的教程吧,做你自己的多人纸牌游戏,用你那神乎其神的牌技,给你的小伙伴们留下深刻印象。

    介绍: snap!


    你将要做的这个纸牌游戏是一个叫做的snap的儿童游戏。这就是你最后完成游戏的画面。

    image

    什么,你还不清楚游戏的规则!好吧,玩这个游戏,需要2到4个人,使用52张牌,通过卡片配对的方式来赢牌,你的目标就是赢得所有牌。

    在每轮开始前,都会重新洗牌,然后顺时针依次发牌,直到牌发完为止。这些牌都会正面朝下摆在玩家面前。

    玩家顺时针依次翻牌。如果轮到你了,翻过你最上方的牌,如果你看到翻开的牌中有能和你的牌形成配对的时候,快速大喊一声"snap",则匹配成功。两个张牌具有相同的值,就能匹配,比如两张王,不管大王还是小王。

    最快速喊出"snap!"并且两张牌确实匹配的那个玩家赢得这两张牌,然后将这两张牌放入自己正面朝下的那些牌中。直到一个玩家赢得了所有牌。如果玩家喊出了"snap!",但是并没有可匹配的牌,那么他要给其他每个玩家一张牌作为惩罚。

    基本流程


    这是一个通过蓝牙或Wi-Fi连接的多人游戏,你将使用GameKit框架来实现它。这里只用到了GameKit的peer-to-peer连接的特性,并没有使用Game Center相关知识,其实这个教程主要使用的只有一个类:GKsession。

    在该教程的第一部分,你将学到如何连接玩家的设备,让这些设备可以使用Snap通过蓝牙或Wi-Fi传递信息。玩这个游戏呢,总要有个人先建立游戏,作为游戏的"服务器",其他玩家作为"客户端",加到这个已经建好的服务器中来。

    这个游戏的流程大体是下面这个样子:

    image

    上面这张图就是游戏的主屏幕,打开游戏玩家首先看到的画面。如何你想玩一局,你可以建立游戏,让其他玩家加进来,或者加进别人建立的游戏,还可以一个人玩单机模式。

    image

    这个"Host Game"画面列出了已经加进来的玩家,点击start按钮开始游戏;从这一刻起,其他没有加进来的玩家就不能在进入到该局游戏了。在玩游戏前,通常玩家都约定好了谁来建立游戏,然后其他玩家加入。

    image

    "Join Game"画面很像Host Game画面,唯一的差别就是这个界面诶有开始按钮,这个tableview列出了可以加入的游戏(列表里很有可能列了多个游戏)。选择一个你想加入的,然后等待主机点击他画面上的开始按钮。

    image

    game screen画面展示的是玩家们都围着桌子坐下,桌子上拍着各自牌面朝上和朝下的牌。点击屏幕右下角的按钮可以发出"snap!"的响声(玩游戏时,你没必要让满屋子都是那响声)。当有玩家按下"snap"按钮时,该玩家昵称旁边会出现一个气泡。

    项目开始


    为了节省你时间,我已经建好了一个带有图片资源和一些nib文件的项目,在这里下载源代码,用Xcode打开Snap.xcodeproj。

    如果你看了源代码,你会发现,项目里只有一个view controller,MainViewController。运行项目,你会发现界面非常简单。

    image

    这个界面有5个UIImageView对象,组成一个logo(S,N,A,P和大王),还有3个UIButton。你可以在这些imageView上做些简单动画,让其更加生动一些,哦,不过你最好先把这些按钮弄好看些。

    你下载的文件还有一个叫做Action_Man.ttf的文件。这是你将在项目中用到的字体文件,他将代替系统的标准字体Helvetica或者你iPhone的内置字体。如果你在Mac下双击这个文件,这个文件将会在字体库中被打开:

    image

    如果你问我,问什么要用这种字体,因为我觉得这种字体看起来更能让人兴奋。但不幸的是这种字体不能装在Mac上,也就是意味着不能在Interface Builder中使用,你必须在代码里面进行控制。然而,首先,你需要告诉UIKit有这种字体,这样app才能加载它。

    在Xcode中打开Snap-Info.plist文件,添加一行,key选中"Fonts provided by application",值为数组类型。把第一项设置成那个字体文件的名字Action_Man.ttf :

    image

    你还需要将那个TTF字体文件加到项目中去,将文件拖拽至Supporting Files下:

    image

    注意,你需要确保Add to Targets这一项是选中的,要不然,字体文件是不会被包含在项目中的。

    image

    现在你可以想下面这样给你的button和label设置字体了:

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

    为了避免代码重复,我们创建个类别。打开File菜单,选择New->File…选项,然后选择"Objcect-C category"模板。创建一个"UIFont"类的类别"SnapAdditions":

    image

    这样将会创建两个文件,UIFont+SnapAdditions.hUIFont+SnapAddtions.m。为了保持项目结构整洁,我将这两个文件加进了刚创建的Categories组中。

    image

    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
    

    这只是一个简单的类别,给UIFont类加了个方法:rw_snapFontWithSize:,这个方法可以用Action Man文件创建一个UIFont对象。

    注意,这个字体文件的名字时Action_Man.ttf,是带有下划线的,但是这种字体的名字是没有下划线的,所以你应该用字体的名字,而不是文件名字。要找到字体的名字,双击它,该文件将会在字体库中打开。(注意,再打开的窗口上方显示的就是字体的名字Action Man而不是Action_Man。)

    第二个要注意的就是,我在这个方法前加了"rw_"。在给标准库中的类添加类别时,在方法前加前缀(或者其它的唯一标示符)是个不错的注意。做这些是为了不和苹果内置的或者将来要添加的方法有冲突。虽然看起来苹果不会去内置一个snapFontWithSize:方法,但是为了安全考虑总是没错的。

    这些准备工作之后,你就可以给你Main View Controller上的button设置字体了。在MainViewController.m的最上方,导入类别文件:

    #import "UIFont+SnapAdditions.h"
    

    viewDidLoad方法中实现如下内容:

    - (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];
    }
    

    现在运行项目。按钮上应该显示了新的字体:

    image

    如果你看到的仍然是老的字体,那么在检查一下Action_Man.ttf是否放入了项目中。选中文件,确保在Target membership这一项是选中的(在Xcode窗口右边的Inspector面板中):

    image

    注意:在使用任何字体的时候都要仔细阅读它的的许可证书,字体文件是受版权保护的,如果你想把它作为你项目中的一部分一起发布,往往是要收取费用的, 但幸运的是Action Man字体是可以免费使用和发布的。

    现在这些按钮看起来好多了,但是一个好的按钮还要有个边框,我们用一些拉伸的图片来给按钮加上边框。这些图片已经加在项目里了,叫做Button.pngButtonPressed.png

    因为这几个不同的画面都需要同样样式的按钮,所以你最好把这个自定义样式的功能放到类别中去。

    给项目添加一个新的类别,叫做"SnapAdditions",但是这次类别是加在UIButton类上。这样就又创建了两个文件,UIButton+SnapAddtions.hUIButton+SnapAdditions.m,然后把这两个文件放到Categories组中,用下面的代码替换.h中的内容:

    @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_"前缀的方法。当你调用这个方法作用到按钮上时,它将会给按钮一个新的背景图片和新的字体样式。

    MainViewController.m中引入新的类别文件:

    #import "UIButton+SnapAdditions.h"
    

    现在你可以用下面的代码替换viewDidLoad中的内容了:

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

    再次运行项目,现在按钮是这个样子的了:

    image

    动画介绍


    现在,你应该让主画面活跃一些。在游戏启动的时候,让logo卡片飞进来如何?

    我们将下面的代码加入MainViewControllerm中来实现这个效果:

    - (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,只是简单的隐藏带有logo的卡片。实际的动画在performIntroAnimation方法中。首先,将卡片放置在屏幕以外,水平居中且在屏幕下方。然后,通过实现动画的block让卡片移动到最终的位置。这样看起来就这些卡片就像是从中间散开一样。

    分别在viewWillAppear:viewDidApper:中调用这两个方法:

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

    现在,启动游戏,这些卡片会飞进屏幕并散开,是不是很cool,哈哈。

    image

    仅仅这些动画还不够完美。我想,当卡片飞向目标位置时,让按钮渐渐出来,这样效果会更好。将下面方法中的几行代码放在prepareForIntroAnimation:方法的最后:

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

    上面这些代码是为了让按钮先透明。将下面的block放到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和多人游戏


    GameKit是iOS SDK中一个标准的framework。它主要用于Game Center(在这篇教程中不会用到)和语音聊天,但是也有在多台设备之间peer-to-peer连接的通讯的特性。如果所有的设备都在同一个Wi-Fi网络环境,GameKit还可以用Wi-Fi来代替蓝牙。(这是通过网络实现peer-to-peer的方式,而且你必须花些时间自己实现大部分代码,这种方式更擅长被用于Game Center。)

    GameKit的peer-to-peer特性,对于在同一个房间,玩家们各自使用自己的设备玩游戏是非常棒的。但是据说,玩家在使用蓝牙的时候,他们之间的距离不能超过10米(或30步)。

    那么,到底什么是peer-to-peer连接呢?每个参与GameKit网络会话的设备称作一个"peer"。一个设备可以作为提供服务的"server",也可以作为寻找服务器的"client",或者即作为服务器又作为客户端。GameKit是使用Bonjour技术来实现这些的,但是你并不需要直接使用Bonjour,因为使用封装好的GameKit就可以了。

    当你使用蓝牙时,设备并不一定要配对,就像用蓝牙鼠标或者键盘跟你的设备配对一样。GameKit很简单地就可以实现客户端和服务器的连接,一旦连接,设备之间就可以通过本地网络发送信息了。

    你不能够选择是使用蓝牙还是Wi-Fi;GameKit会为你选择的。模拟器是不支持蓝牙的,但是支持Wi-Fi。

    在开发和测试这篇教程时,我发现使用模拟器和一两个真机通过本地Wi-Fi进行连接,就可以很容易地实现多人玩游戏。如果你要想通过蓝牙来玩,那就需要至少两个带有蓝牙功能的真机了。

    注意:其实不用GameKit框架,使用Bonjour和蓝牙也可以实现网络通讯,但是,如果你想创建一个多人游戏,使用GameKit是非常简单的。它隐藏了很多让你非常厌恶的网络开发的东西,并且给你封装了一个简单的类GKSession来使用。这个类也是在该教程中,涉及到GameKit的唯一的类(和它的委托,GKSessionDelegate)。

    如果你只想做一个通过蓝牙或者Wi-Fi来玩的两人游戏,你可以使用GameKit的GKPeerPickerController来创建设备间的连接。就像下面这个样子。

    image

    GKPeerPickerController的使用很简单,但是只能两台设备连接。但是Snap!需要同时四个人一起玩,所以通过这篇教程来教你通过自己写代码实现多人连接。

    "Host Game"界面


    在这部分,你将添加一个"Host Game"界面。这个界面允许玩家建立一个房间,让其他玩家加进来。当你完成的时候,应该是这个样子:

    image

    这里有个列出了连接到当局游戏的玩家的列表,一个开始按钮,还有个可以输入玩家昵称的文本框(默认里面是你机器的名字)。

    添加一个UIViewController的子类,命名为HostViewController。创建时不要选中"With XIB for user interface"选项。这个界面上的基本控件已经在一个xib文件中摆放好了,你可以在"Snap/en.lproj"文件夹中找到这个xib文件。把HostViewController.xib加到项目中。这个xib文件打开后是下面这个样子:

    image

    这些UI控件都有跟代码里的属性和方法相关联,因此,你应该把这些控件和HostViewController类关联起来,否则,项目运行起来去加载xib时会挂掉的。

    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文件中。这是新版Xcode(4.2或者更高)中LLVM编译器的新特性。这样可以使你的.h文件更加简洁明,可以把一些不想被别的类看到的属性隐藏起来。

    当然,你还需要synthesize这些属性,将下面的代码添加到@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;
    }
    

    最后,用下面这行代码替换HostViewController.h文件中的@interface这行:

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

    Host Game界面的基本工作已经做完,但是当用户点击主界面上的按钮时,还需要触发动作显示Host Game界面。将下面的代码添加到MainViewController.h中:

    #import "HostViewController.h"
    

    并且像下面这样实现MainViewContoller.m文件中的HostGameAction:方法:

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

    如果你现在启动app,点击Host Game按钮,屏幕上将会呈现Host Game界面,但是这还不够炫。你用modal的方式来显示这个新的viewController,但是没有把animated这个参数设置为YES,所以也就没有从下往上滑动的效果。

    因为像这样的动画用在这里并不是太好,所以我们并没有使用让Host Game界面从主界面向上滑动出现的效果,我们在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文件或者.m扩展中声明,或者将该方法放在使用的代码前面。现在不需要了,这一切都要感谢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];
            }];
        }
    }
    

    代码跟以前差不多,但是现在的逻辑是:将创建和呈现Host Game界面的代码放在了一个动画执行之后会调用的block中。运行项目看看效果吧。你把动画的代码放在了一个单独的方法中,当这样用户点击其它按钮时,也可以用这个方法来做动画。

    正如你所看到的,Host Game界面使用的还是默认的Helvetica字体,并且它的开始按钮也没有边框。其实很容易就可以改好。将下面两个头文件导入到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];
    }
    

    因为你创建了这些类别,所以可以很方便地添加字体和按钮样式,真是不费吹灰之力就让界面如此漂亮,哈哈(原句: it’s a snap (ha ha) to make the screen look good)。运行项目看看效果吧。

    现在还有点东西要改进。这里有个让用户输入名字的文本框,当用户点击时,在界面的最上方会弹出个键盘。这个键盘盖住了半个屏幕,关键是现在还没有办法让它下去。

    image

    第一种让键盘消失的方法:就是用那个又大又蓝带有"Done"(中文状态下应该是"完成")的按钮。现在你点击之后是什么都不会发生的,不过将下面的方法加入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,否则界面上的其它控件都不会有任何事件响应了,比如列表,按钮。:

    退出Host Screen界面


    除了开始游戏按钮,其它的还都没有事件。现在我们要做的就是点击Host Screen界面左下角的x按钮,让屏幕重新回到主界面。

    这个按钮是跟exitAction:方法绑定的,现在里面还是空的。点击它应该能够关闭这个界面,你将用delegate来实现这些。在这个项目中有几个viewController, 你将用delegate来控制它们的切换。

    将下面的代码添加到HostViewController.h,放在@interface这行之前:

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

    在@interface里加入这个新的属性:

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

    属性需要synthesize,因此将下面的代码放在HostViewController.m中:

    @synthesize delegate = _delegate;
    

    最后,用下面的方法替换exitAction:方法:

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

    想法很明确了:在HostViewController中声明一个delegate protocol。当用户点击了x按钮时,HostViewController会告诉delegate,Host Game界面已经不需要了。然后delegate会履行自己的职责,关掉界面。

    在这里,MainViewController扮演着delegate的角色,当然,我们还需要在MainViewController.h中添加<HostViewControllerDelegate>

    @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的delegate。最后,在MainViewController.m文件中实现delegate方法:

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

    没有使用动画,简单地关掉了HostViewController界面。但是由于MainViewController的viewWillAppear方法被再次调用,卡片飞进来的动画就被执行了一次。运行项目试试看。

    注意:在调试时,当一个界面消失的时候,我想确保viewController被销毁,所以我在viewController中加入dealloc方法,再次方法中输出信息到控制台。

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

    即使你在项目中使用了ARC,项目仍然有可能有内存泄露的地方。虽然ARC是个非常好的内存管理工具,但是它无法解决循环引用的问题。比如你有两个对象,都有个强类型指针指向对方,这样它们将永远留在内存中。这就是我为什么在dealloc中输出log,确保对象对象被销毁的原因,只是为了擦亮自己的眼睛,把事情搞得更清楚。

    现在,Host Game界面已经完成。在你建立想加入或者建立一局之前,你必须先进入游戏界面。否则,其它设备找不到你的设备!

    "Join Game"界面


    这个界面跟Host Game界面很相似,但是鉴于它们那些不同之处,我们足以有理由去创建一个新的类(不是继承hostViewController)。由于跟之前做的很类似,所以你可以很快地完成这些。

    创建一个UIViewController的子类JoinViewController。创建时不要带有xib,因为我已经在你开始用的代码里提供了,将这个xib文件(在"Snap/en.lproj/"这里)添加到项目中。

    用这些代替JoinViewController.h文件的内容:

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

    就像当初对Host Game界面所做的那样,用下面的代码替换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",因为它在这个xib文件中属于top-level视图:

    image

    将这个属性声明为strong是及其重要的,这样可以避免被销毁。你没必要对第一个视图做这样的操作,因为viewController内置的self.view属性已经retain了。

    当用户点击了列表中别人建立的游戏房间名时,我么在主页面上方放置了另一个视图(就是画面上显示"Connecting..."的视图)。这本来可以用一个新的viewController来做,但是为了简单,我们先这样做了。

    修改MainViewController.n文件:

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

    MainViewController.m中,用下面的方法替换JoinGameAction:

    - (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];
            }];
        }
    }
    

    并且实现如下delegate方法:

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

    现在我们已经完成了Join Game界面。是时候添加配对逻辑了。

    注意:当你写多人游戏(或者其它基于网络通讯的软件)时,都有两种架构选择:client-server和peer-to-peer(这里的意思是点对点方式)。尽管我们可以使用GameKit的"peer-to-peer"方式,但是这次我们选择client-server方式。一个玩家作为服务器,其它玩家加进来。

    image

    在client-server这种模式下,server控制着所有事情并且决定卡片是否配对。client发送玩家的更新到server,server通知所有client更新,client之间并不进行直接数据交流。然而在真正让点对点模式下,所有的client都是平等的,做相同的工作,但是你一定要确保所有的玩家看到同样的事情,因为这种方式没有server来控制。 
    再次说一次,在这篇教程中使用的是client-server模式,这种模式需要一个玩家来作为服务器。

    牌型配对


    现在你的Host Game和Join Game两个界面基本上都能用了,现在你可以添加卡片配对逻辑了。当一个玩家点击Host Game界面上的按钮时,他的设备会被别的玩家在Snap中搜索到。当其他玩家进入Join Game界面时,能够看到很多server。

    虽然GameKit的GKSession类为这些做了很多,但是你仍然需要做很多工作。我们创建了两个类MatchmakingServer和MatchmakingCient来处理游戏的逻辑,而不是把所有的逻辑都放在viewController里。viewController承受东西太多的话,代码看起来会非常的凌乱。这就是我为什么还要创建两个新对象来管理设备间通讯的原因。

    在创建这些新的类之前,你应该先把GameKit框架添加进项目中。在Target Summary界面,Linked Frameworks and Libraries里,点击+按钮,在弹出的列表中选择GameKit.framework加到项目中。

    image

    因为我们要在很多文件中用到GameKit这个框架,所以我们没有像平常那样在各个源代码文件上方导入GameKit,而是在预编译头文件中导入GameKit框架,也就是Snap-Prefix.pch文件(在Supporting Files中)中的#ifdef __OBJC__部分加入下面这行代码:

    #import <GameKit/GameKit.h>
    

    现在所有的文件中都可以用GameKit框架了。

    你还有一件事要做,那就是在Info.plist文件中表示这个项目用了peer-to-peer功能,因为并不是所有的设备(尤其是第一代的iPhone和iPod Touch)都支持peer-to-peer功能。

    打开Snap-Info.plist文件并在"Required device capabilities"下加入一个子项,并设置其值为"peer-peer":

    image

    MatchmakingServer


    创建一个新的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
    

    MatchmakingServer有一个连接其它client的列表,并且要有一个变量来控制同一时间连接进来的client数量。就是该游戏每局有最多4个人玩家的限制,也就是只能有3个玩家连接进来(算上自己正好是4个)。

    MatchmakingServer还有个GKSession对象,来控制各个设备之间的网络通讯,MatchmakingServer还要遵守GKSessionDelegate协议,因为这样GKSession可以告诉它一些重要的事件。

    现在,MatchmakingServer还只有一个方法,用来进行广播服务和接收client的消息。很快,你就会往这个类里加很多东西。

    用下面的代码替换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方法除了在Xcode Debug面板上打出了一些log之外,没有做任何事情。不过在startAcceptingConnectionsForSessionID方法中倒是有些新鲜的东西:

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

    你在这里创建了GKSessin对象,并且设置好delegate在这里管理。说详细些就是只对有效的service(以sessionID参数命名的)进行广播服务,不会关心其它任何广播同样消息的设备。你告诉session,MatchmakingServer是它的delegate,然后设置了"availabel"属性的值为YES,这样就可以开启广播了。这就是你让GameKit session所做的事情。

    现在你要把MatchMakingServer导入到HostViewController.h文件中:

    #import "MatchmakingServer.h"
    

    添加一个MatchmakingServer对象作为HostViewController的一个实例变量,如下:

    @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];
        }
    }
    

    一旦Host Game界面出现,就会创建一个MatchmakingServer对象,并且告诉它开始接受连接。同时它会把你机器的名字作为placeholder填入"Your Name"文本框中,如果你不输入你的名字,那么就用这个来作为你在游戏中的标示。

    在你定义SESSION_ID之前,新的代码是不会起作用的。不用太关心SESSION_ID的内容是什么,只要server和client保持同样地值就可以了。GameKit将用这个值作为唯一Bonjour标示。因为MatchmakingServer和MatchmakingClient都会用到这个SESSION_ID,所以最好还是把它定义在prefix文件中吧。打开Snap-Prefix.pch并且把下面这行代码放在文件的最后:

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

    运行项目,点击Host Game界面上的按钮。如果你是运行在模拟器中,你将会看到下面这个界面:

    image

    GKSession中displayName属性的值是像这样"com.hollance.Sanp355561232..."的一串字符串,如果你在你自己的设备上运行,上面显示的将是你设备的名字,比如:"Joe's iPhone"或者你一开始给你设备设置的名字。

    你现在已经有一个运行良好,可以广播"Snap!"服务的server了,但是还没有client连接进来。现在我们就创建一个MatchmakingClient类来实现这些。

    MatchingmakingClient


    添加一个新的类MatchmakingClient,继承NSObject,并把它放到Networking组。用下面的代码替换MatchmakingClient.h文件的内容:

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

    就像是从MatchmakingServer这面镜子映出来的一样,但是这个列表里不是client,而是server。用下面的代码替换MatchmakingClient.m文件的内容:

    #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
    

    又一次,我们给类搭了个框架,然后去填好这些方法。注意,你创建了个GKSessionModeClient模式的GKSession对象,因此它会寻找有效的server(不是自己广播的服务)。

    现在将新创建的类整合到JoinViewController中。这样server和client才能连接。首先导入头文件到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网络,用你的模拟器和真机连接。一个设备点击进入Host Game界面,另一个点击设备进入Join Game界面。

    界面上什么都没有发生,但是debug窗口输出了很多log:

    server输出内容:

    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
    

    这些都是GameKit发出的消息。client也可以从GameKit发出消息,但是client输出的是:

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

    这个消息来自GKSessionDelegate的session:peer:didChangeState:方法,在你的MatchmakingClient类里面。他告诉我们ID为"663723729"的小伙伴已经准备好了,也就是说,client侦测到了有效的server。

    注意:peer ID是GameKit生成用来在一次会话中区别不同设备的标示。每次启动游戏,这个ID都会变的。很快你就会用到这些peer ID。

    如果你有多台设备,你就可以创建多个server。但对于这篇教程,一个client只需要连接一个server就可以了,但是他们可以相互侦测到对方。试试吧!

    到了这里,该何去何从?


    到现在为止,所有的范例代码都在这里

    恭喜,你现在有了一个漂亮的画面,app中的按钮动画也很流畅,Host Game和Join Game界面也都基本实现。另外,也可以用GameKit和Bonjour来广播和侦测server了!

    非常棒,但是很明显,你要在屏幕上显示搜索到的server,这样用户才能选择你的server加入游戏。这就是该系列教程在第二部分内容。

    如何你对于本篇文件有任何问题或者评论,请在下面加入我们的讨论!

  • 相关阅读:
    布局管理器
    下拉列表框
    时间,日期选择器
    关于部分基本控件的使用
    关于Activity
    什么时候修改class
    JavaScript Break 和 Continue 语句
    JavaScript While 循环
    JavaScript For 循环
    JavaScript Switch 语句
  • 原文地址:https://www.cnblogs.com/jiangshiyong/p/3330397.html
Copyright © 2011-2022 走看看