iphone__game center 多人游戏

免责申明(必读!):本博客提供的所有教程的翻译原稿均来自于互联网,仅供学习交流之用,切勿进行商业传播。同时,转载时不要移除本申明。如产生任何纠纷,均与本博客所有人、发表该翻译稿之人无任何关系。谢谢合作!

原文链接地址:http://www.raywenderlich.com/3276/how-to-make-a-simple-multiplayer-game-with-game-center-tutorial-part-12

程序截图:

  我正在实验以一种新的方式来撰写教程--通过采纳你们的建议来写教程!

  在网站右边,你会发现一个新的区域,在那里,你们可以通过投票的方式来决定下一篇教程是什么。(当然,在原作者网站上面,我这里没有啦:)

  在第一次投票中,你们中的好多人说,想让我写一篇关于如何制作一个简单的多人在线游戏教程---现在满足你们的要求!

  在这个2部分系列教程中,你将会学习到如何使用cocos2d和game center来做一个简单的2人联机玩的小游戏。

  这个游戏非常简单,就是一只狗与一只猫在比赛跑步---你点屏幕点得越快就越容易赢得比赛!

  这个教程假设你对于cocos2d的基础知识已经非常熟悉了。如果你对于cocos2d完全陌生的话,你可以需要先看看这个网站里的其它教程

  注意:为了完整实践本系列教程,你必须注册iOS开发者,这样的话,你才可以激活Game Center。当然,你将至少需要一台物理设备(这样的话,你就可以运行一个程序在模拟器上面,另一个程序在你的设备上面啦)。最后,你将至少需要2个不同的Game Center帐号来测试(不用担心,你可以创建n个免费的帐号,只需要提供不同的邮件地址就ok了)

  准备好了吗?出发!

Getting Started

  这个教程将教你如何向一个简单的游戏里面添加matchmaking和多人在线支持。

  因为游戏逻辑并不是本教程的重点,所以,我已经准备好了一些代码,只是还没有联网功能。

  下载上面链接中的代码,编译并运行,你将会看到下面的游戏截屏:

  这个游戏非常简单,而且代码注释良好---你可以直接去研究代码,但是要确保你能够看懂每一行代码。

  如果你们对些代码感兴趣的话,我可以再单独写一个教程来教大家如何从头开始构建一个这样的游戏。(如果你愿意,请到原作者的网站上面去投票!)

激活Game Center:总览

  现在,你已经有了一个非常简单的可以玩的游戏了,但是,它很无聊,因为你老是自己跟自己玩!

  如何使用Game Center的话,这个游戏会变得灰常有趣,因为你可以邀请朋友和你一起玩,或者使用matchmaking来随机查找线上的玩家。

  但是,在你开始写任何Game Center代码之前,你需要做以下2件事情:

  1. 创建并设置一个App ID
  2. 在 iTunes Connect 里面注册你的app

  让我们一步步来:

创建和设置一个App ID

  第一步就是创建并设置一个App ID。首先,你需要登录到 iOS Dev Center,然后进到 iOS Provisioning Portal。

  在那里,选择App IDs标签,然后为你的应用程序创建一个App ID,和下面的图类似(需要你填写的值可能有差异)。

 

  最重要的部分就是Bundle Identifier--你需要设置它为一个唯一的字符串(因此,你不能使用这个教程里我所使用过的!!!)最佳做法是使用你的域名来避免名字冲突。

  一旦完成之后,点击Submit。然后打开Cat Race Xcode工程,选择Resources\Info.plist,然后输入你在iOS Provisioning portal里面输入的任何唯一的字符串,如下所示:(注意,要填写你自己的)

  最后一件事,Xcode有时候会出问题,特别是在你更改了bundle identifier之后,因此,为了保证万无一失,你需要做以下3步:

  • 删除你的模拟器或者设备上面的Cat Race程序
  • 如果模拟器正在运行的话就退出来。
  • 点Project\Clean来清理工程

  恭喜---现在你已经为你的应用程序创建App ID了,而且接下来会使用到它。下面,你可以通过ITunes Connect注册你的应用程序并激活Game Center。

在Itunes Connect中注册你的程序

  接下来,登录到  iTunes Connect(只有Team Agent账号可以登录)并为你的应用创建一个新的入口(entry)。

  一旦你登录入到iTunes Connect以后,选择Manage Your Applications,然后点左上角的蓝色的“Add New App”按钮。

  在出现的第一个屏幕中,在App Name中输入Cat Race,SKU Number中输入CRI,然后选择你之前创建的id,类似于下面的截屏:

  点continue,并按照提示输入关于你的app的一些基本信息。

  不用担心你填写的值对不对,尽管乱填,因为你之后还是可以改的---你只需要往里面添加一些傻瓜式的icon和screenshoot就可以让iTunes Connect很happy了。

  当你做玩这些之后,点Save。如果一切ok,你将会到达“Prepare for Upload”阶段,如下图所示:

  点右上角的蓝色的“Manage Game Center”按钮,然后点“Enable”按钮,再点“Done”。就这么多,你的app的Game Center功能已经激活了,接下来是时候写一些代码了。

  顺便提一下,在“Manager Game Center”部分,你可能注意到了Leaderboards和Achievments等选项。不过,这篇教程中,我们并不会介绍它们,但是,如果你们对此感兴趣的话,你可以在我即将出版的里找到。

认证本地的用户:策略

  当你的游戏开始的时候,第一件事你需要做的就是认证本地玩家。

  你可以把它看作是“把玩家添加进Game Center”。如果他已经登录了的话,那么会收到“Welcome Back!”消息。否则,它会要求玩家输入用户名和密码。

  认证本地用户是非常容易的---你只需要调用 authenticateWithCompletionHandler就可以了。你可以选择性地传入一个block,当用户被认证身份以后就会回调这个block。(block是ios的新特性,ios3.0之前是不能使用的)

  但是,这里有个技巧。还有另外一种方式让用户登录和登出。他可以先打开你的app,然后切换到Game Center app,从这里登录或登出,然后回到你的app。

  因此,你的app在用户认证状态改变的时候应该得到通知。你可以注册一个“authentication changed” notification。

  因此,我们的策略来认证用户的过程如下:

  • 创建一个单例类来管理所有与Game Center相关的代码.
  • 当单例对象创建的时候,它会注册“authentication changed” notification。
  • 游戏将调用单例对象上的一个方法来认证用户。
  • 不管什么时候用户被认证(或登出),“authentication changed”回调将会触发。
  • 这个回调将会追踪用户当前是否被认证。

  现在,你已经知道怎么做啦,让我们直接coding吧!

认证本地用户:实现

  打开Cat Race项目,点  File\New\New File,然后选择  iOS\Cocoa Touch\Objective-C class,再点Next。选择NSObject作为基类,再点Next,把它命名为GCHelper,然后点Finish。

  把GCHelper.h换成下面的形式:

#import   < Foundation / Foundation.h >
#import   < GameKit / GameKit.h >

@interface  GCHelper : NSObject {
BOOL gameCenterAvailable;
BOOL userAuthenticated;
}

@property (assign, 
readonly ) BOOL gameCenterAvailable;

+  (GCHelper  * )sharedInstance;
-  ( void )authenticateLocalUser;

@end

  这里导入了GameKit头文件,然后定义了两个bool型的实例变量--一个用来追踪设备是否支持game center,还有一个用来追踪当前用户是否被认证。

  我们也需要创建property,这样的话就可以直接查看game center是否可用。同时,还需要定义一个静态方法用来创建单例,还有一个认证本地用户的方法(这个方法会在app启动的时候被调用)

  接下来,回到GCHelper.m,然后替换成下面的样子:

@synthesize  gameCenterAvailable;

#pragma  mark Initialization

static  GCHelper  * sharedHelper  =  nil;
+  (GCHelper  * ) sharedInstance {
if  ( ! sharedHelper) {
sharedHelper 
=  [[GCHelper alloc] init];
}
return  sharedHelper;
}

  这里synthesize gameCenterAvailable属性,然后定义了单例方法的实现。

  注意,有很多方式可以实现单例方法,但是,我们这里使用了最简单,我们没有考虑多线程的情况。

  接下来,在sharedInstance方法后面加入下列代码:

-  (BOOL)isGameCenterAvailable {
//  check for presence of GKLocalPlayer API
Class gcClass  =  (NSClassFromString( @" GKLocalPlayer " ));

//  check if the device is running iOS 4.1 or later
NSString  * reqSysVer  =   @" 4.1 " ;
NSString 
* currSysVer  =  [[UIDevice currentDevice] systemVersion];
BOOL osVersionSupported 
=  ([currSysVer compare:reqSysVer 
options:NSNumericSearch] 
!=  NSOrderedAscending);

return  (gcClass  &&  osVersionSupported);
}

  这个方法是直接从苹果的 Game Kit Programming Guide中copy过来的。它用来检测当前设备是否支持game center。

  在使用game center之前必须要判断其是否可用,这和网络编程一样,没有网的情况一定要判断。同时,这个app只能运行ios4.0及其以后的系统上面。

  接下来,在 isGameCenterAvailable方法后面添加下列代码:

-  ( id )init {
if  ((self  =  [super init])) {
gameCenterAvailable 
=  [self isGameCenterAvailable];
if  (gameCenterAvailable) {
NSNotificationCenter 
* nc  =  
[NSNotificationCenter defaultCenter];
[nc addObserver:self 
selector:@selector(authenticationChanged) 
name:GKPlayerAuthenticationDidChangeNotificationName 
object :nil];
}
}
return  self;
}

-  ( void )authenticationChanged { 

if  ([GKLocalPlayer localPlayer].isAuthenticated  &&   ! userAuthenticated) {
NSLog(
@" Authentication changed: player authenticated. " );
userAuthenticated 
=  TRUE; 
else   if  ( ! [GKLocalPlayer localPlayer].isAuthenticated  &&  userAuthenticated) {
NSLog(
@" Authentication changed: player not authenticated " );
userAuthenticated 
=  FALSE;
}

}

  init方法检测Game Center是否可用,如果可用,则注册  “authentication changed” notification。(这是非常经典的观察者模式)。在尝试认证用户之前,注册这个通告灰常重要,这样,当认证完成的时候,它就会被调用。

  这里的 authenticationChanged回调函数是很简单的--它只是简单地判断用户是否被认证,并且相应地更新标记变量。

  注意,实际上,这个回调可能会被调用许多次,所以要确保 userAuthenticated和之前的状态不一样,只有当上一次状态改变的时候才更新。(具体理解参考代码)

  最后,在 authenticationChanged方法后面添加下面方法:

#pragma  mark User functions

-  ( void )authenticateLocalUser { 

if  ( ! gameCenterAvailable)  return ;

NSLog(
@" Authenticating local user... " );
if  ([GKLocalPlayer localPlayer].authenticated  ==  NO) { 
[[GKLocalPlayer localPlayer] authenticateWithCompletionHandler:nil]; 
else  {
NSLog(
@" Already authenticated! " );
}
}

  这里调用前面提到过的 authenticateWithCompletionHandler来认证用户。注意,这里目前并没有传入一个完整的处理器(传入的是nil)。因为,前面你已经注意了  “authentication changed”notification了,所以这里就没有必要再写一个handler了。

  好了--GCHelper现在包含认证用户所需的所有代码了,所以,你是时候使用它们了!回到AppDelegate.,然后做如下更改:

//  At the top of the file
#import   " GCHelper.h "

//  At the end of applicationDidFinishLaunching, right before 
//  the last line that calls runWithScene:
[[GCHelper sharedInstance] authenticateLocalUser];

  这里创建GCHelper的单例(它的init方法里面注册了 “authentication changed”通告)并调用 authenticateLocalUser 方法。

  差不多快完成了!最后一步就是添加Game Kit framework到你的工程中。我们先点工程文件,然后选择Build Phases 标签,展开 “Link Binary with Libraries”,再点”+“号来添加相应的framework。选择GameKit.framework并点Add。把type改成Required,然后看起来如下图所示:

  就这么多!编译并运行工程,如果你登入Game Center,你将会看到下面的输出:

  现在,你已经认证用户了,你可以进而探寻更有趣的部分了,比如找一个人与你共同来玩这个游戏!

Matchmaker, Matchmaker, 给我找个对手吧!

  这里有两种方法可以通过Game Center来找一些人来一起玩游戏:一种是编程来主动查找,另一种是使用内置的matchmaking接口。

  在这篇教程中,我们将使用内置的matchmaking接口。当你想要寻找一个对手的时候,只需要在 GKMatchRequest对象上面设置几个参数,然后再创建并显示一个 GKMatchmakerViewController就ok了。

  让我们来看一看这个具体是怎么工作的。首先在GCHelper.h中做一些修改:

//  Add to top of file
@protocol  GCHelperDelegate 
-  ( void )matchStarted;
-  ( void )matchEnded;
-  ( void )match:(GKMatch  * )match didReceiveData:(NSData  * )data 
fromPlayer:(NSString 
* )playerID;
@end

//  Modify @interface line to support protocols as follows
@interface  GCHelper : NSObject  < GKMatchmakerViewControllerDelegate, GKMatchDelegate >  {

//  Add inside @interface
UIViewController  * presentingViewController;
GKMatch 
* match;
BOOL matchStarted;
id   < GCHelperDelegate >   delegate ;

//  Add after @interface
@property (retain) UIViewController  * presentingViewController;
@property (retain) GKMatch 
* match;
@property (assign) 
id   < GCHelperDelegate >   delegate ;

-  ( void )findMatchWithMinPlayers:( int )minPlayers maxPlayers:( int )maxPlayers 
viewController:(UIViewController 
* )viewController 
delegate :( id < GCHelperDelegate > )theDelegate;

这里出现了一些新的内容,让我们一点一点来看:

  • 你定义了一个协议,叫做 GCHelperDelegate。当match开始,结束,或者从第三方接收到数据的时候就会通知其它对象,当然前提是那个对象要实现该协议。在本例中,cocos2d的layer将会实现此协议。
  • 同时,GCHelper对象实现了两个协议。第一个是matchmaker进行玩家查找,不管有没有找到一个新的match,就会通知实现该协议的对象。第二个就是当数据到达或者连接状态改变的时候,Game Center会通知GCHelper对象。
  • 创建一个新的实例变量和相应的属性来追踪view controlller对象(这个对象将会用来显示matchmaker用户界面),一个match对象的引用,match是否开始的标记以及一个代理。
  • 创建一个新的方法,我们之后的cocos2d layer将会调用这个方法来查找可以一起玩游戏的玩家。

  接下来跳转到GCHelper.m文件,然后做如下修改:

//  At top of file
@synthesize  presentingViewController;
@synthesize  match;
@synthesize   delegate ;

//  Add new method, right after authenticateLocalUser
-  ( void )findMatchWithMinPlayers:( int )minPlayers maxPlayers:( int )maxPlayers 
viewController:(UIViewController 
* )viewController 
delegate :( id < GCHelperDelegate > )theDelegate {

if  ( ! gameCenterAvailable)  return ;

matchStarted 
=  NO;
self.match 
=  nil;
self.presentingViewController 
=  viewController;
delegate   =  theDelegate; 
[presentingViewController dismissModalViewControllerAnimated:NO];

GKMatchRequest 
* request  =  [[[GKMatchRequest alloc] init] autorelease]; 
request.minPlayers 
=  minPlayers; 
request.maxPlayers 
=  maxPlayers;

GKMatchmakerViewController 
* mmvc  =  
[[[GKMatchmakerViewController alloc] initWithMatchRequest:request] autorelease]; 
mmvc.matchmakerDelegate 
=  self;

[presentingViewController presentModalViewController:mmvc animated:YES];

}

  这个cocos2d layer将要调用的方法的主要功能就是查找一个玩家。如果Game Center不可用的话,那么就什么也不干,直接返回。

  它首先初始化match为未开始状态,并把match对象设置为nil。并且,存储视图控制器和代码,以便后面使用。同时,还要销毁前面已经出现的任何模态视图控制器(比如: GKMatchmakerViewController已经显示出来了)。

  然后,我们来讲一下重点内容。 GKMatchRequest允许你配置你将要查找的match的类型,比如最小或者最大的玩家数量。这个方法比较灵活,你可以传递任何数量。但是,本游戏只需要设置最小和最大都为2就可以了。

  接下来,我们使用给定的 request来创建一个 GKMatchmakerViewController类的实例,同时把代理设置为GCHelper对象,然后把它显示到屏幕上。

    这时,GKMatchmakerViewController这个类对象的视图就开始接管工作了。它会允许用户查找一个随机的玩家来一起玩游戏。

  接下来,我们需要定义一些代理方法:

#pragma  mark GKMatchmakerViewControllerDelegate

//  The user has cancelled matchmaking
-  ( void )matchmakerViewControllerWasCancelled:(GKMatchmakerViewController  * )viewController {
[presentingViewController dismissModalViewControllerAnimated:YES];
}

//  Matchmaking has failed with an error
-  ( void )matchmakerViewController:(GKMatchmakerViewController  * )viewController didFailWithError:(NSError  * )error {
[presentingViewController dismissModalViewControllerAnimated:YES];
NSLog(
@" Error finding match: %@ " , error.localizedDescription); 
}

//  A peer-to-peer match has been found, the game should start
-  ( void )matchmakerViewController:(GKMatchmakerViewController  * )viewController didFindMatch:(GKMatch  * )theMatch {
[presentingViewController dismissModalViewControllerAnimated:YES];
self.match 
=  theMatch;
match.
delegate   =  self;
if  ( ! matchStarted  &&  match.expectedPlayerCount  ==   0 ) {
NSLog(
@" Ready to start match! " );
}
}

  如果用户取消查找match或者查找过程中出现了错误的话,那么我们需要关闭matchmaker 视图。

  但是,如果找到一个match的话,我们需要隐藏此对象,并且设置match的delegate为GCHelper对象。这样的话,当有新的数据到达,或者连接状态改变的话,GCHelper对象就会得到通知。

  同时,我们也需要检测是否可以开始match了。match对象保存了仍然需要多少个玩家才能完成连接,这个数目由“ expectedPlayerCount”来定。

  如果这个变量是0的话,那么所有人都准备好了。当然,现在我们只是用NSLog输出一些语句,看是否执行到此处了。

  接下来,添加 GKMatchDelegate回调函数实现:

#pragma  mark GKMatchDelegate

//  The match received data sent from the player.
-  ( void )match:(GKMatch  * )theMatch didReceiveData:(NSData  * )data fromPlayer:(NSString  * )playerID { 
if  (match  !=  theMatch)  return ;

[
delegate  match:theMatch didReceiveData:data fromPlayer:playerID];
}

//  The player state changed (eg. connected or disconnected)
-  ( void )match:(GKMatch  * )theMatch player:(NSString  * )playerID didChangeState:(GKPlayerConnectionState)state { 
if  (match  !=  theMatch)  return ;

switch  (state) {
case  GKPlayerStateConnected: 
//  handle a new player connection.
NSLog( @" Player connected! " );

if  ( ! matchStarted  &&  theMatch.expectedPlayerCount  ==   0 ) {
NSLog(
@" Ready to start match! " );
}

break
case  GKPlayerStateDisconnected:
//  a player just disconnected. 
NSLog( @" Player disconnected! " );
matchStarted 
=  NO;
[
delegate  matchEnded];
break ;

}

//  The match was unable to connect with the player due to an error.
-  ( void )match:(GKMatch  * )theMatch connectionWithPlayerFailed:(NSString  * )playerID withError:(NSError  * )error {

if  (match  !=  theMatch)  return ;

NSLog(
@" Failed to connect to player with error: %@ " , error.localizedDescription);
matchStarted 
=  NO;
[
delegate  matchEnded];
}

//  The match was unable to be established with any players due to an error.
-  ( void )match:(GKMatch  * )theMatch didFailWithError:(NSError  * )error {

if  (match  !=  theMatch)  return ;

NSLog(
@" Match failed with error: %@ " , error.localizedDescription);
matchStarted 
=  NO;
[
delegate  matchEnded];
}

   match:didReceiveData:fromPlayer这个方法是在其他玩家给你发送数据的时候被调用的。这个方法只是简单的把这些数据再转发给它的代理类。(我们这个游戏中,cocos2d layer会实现此代码,所以这个代码是跟游戏需要相关的。

   For match:player:didChangState这个方法,是当有玩家接入的时候,你需要检测是否所有的玩家都已经就绪了。同时,当有玩家断开连接的时候,这个方法也会被调用。

  最后两个方法是发生错误的时候被调用。任何一种情形,都把match标记为已经结束了,同时通知delegate对象。

  好了,我们现在写一些代码来建立一个match吧。首先从HelloWorldLayer中开始,打开HelloWorldLayer.h,并做如下修改:

//  Add to top of file
#import   " GCHelper.h "

//  Mark @interface as implementing GCHelperDelegate
@interface  HelloWorldLayer : CCLayer  < GCHelperDelegate >

  然后,跳转到HelloWorldLayer.m中做如下修改:

//  Add to top of file
#import   " AppDelegate.h "
#import   " RootViewController.h "

//  Add to bottom of init method, right after setGameState
AppDelegate  *   delegate   =  (AppDelegate  * ) [UIApplication sharedApplication]. delegate
[[GCHelper sharedInstance] findMatchWithMinPlayers:
2  maxPlayers: 2  viewController: delegate .viewController  delegate :self];

//  Add new methods to bottom of file
#pragma  mark GCHelperDelegate

-  ( void )matchStarted { 
CCLOG(
@" Match started " ); 
}

-  ( void )matchEnded { 
CCLOG(
@" Match ended " ); 
}

-  ( void )match:(GKMatch  * )match didReceiveData:(NSData  * )data fromPlayer:(NSString  * )playerID {
CCLOG(
@" Received data " );
}

  这里最重要的部分就是init方法。它从AppDelegate那里得到一个RootViewController,因为这个视图控制器将会显示出matchmaker界面。然后创建一个GCHelper对象来查找一个match。

  剩下的部分仅仅是一些桩代码,简单的实现了GCHelper协议,同时在里面输出了一些语句。

  最后一件事,默认情况下面,cocos2d 模板并没有在AppDelegate里面包含一个RootViewController的属性,因此你必须手动添加一个。跳转到AppDelegate.h文件,并添加下面的代码:

@property (nonatomic, retain) RootViewController  * viewController;

  然后跳转到AppDelegate.m,synthesize之:

@synthesize  viewController;

  就这么多!编译并运行你的程序,现在你将看到matchmaker视图了,它看起来如下图所示:

  现在,在另一个设备上运行你的程序。当然,你也可以一个运行在模拟器上面,一个运行在iphone上面。

  注意:每一个设备上面需要使用一个不同的game center帐号,否则的话就不能工作。

  在两个设备上都点击“Play Now”,然后,过了一段时间后,matchmaker视图将会消失,接着你将会在控制台输出下面的语句:

CatRace[ 16440 : 207 ] Authentication changed: player authenticated.
CatRace[
16440 : 207 ] Player connected !
CatRace[
16440 : 207 ] Ready to start match !

  恭喜你!你已经在两台设备之间完成了一次match了。你正在制作一个网络游戏,知道吗?:)呵呵

Landscape Orientation and GKMatchmakerViewController

  你可以已经注意到了,默认情况下 GKMatchmakerViewController显示的方向是竖的(portrait)。很明显,这不行,因此,cocos2d模板生成的程序是横版的。

  幸运的是,你可以为 GKMatchmakerViewController写一个类别,让它强制只接收横版方向。

  让我们实现这个,首先点击 File\New\New File,然后选择  iOS\Cocoa Touch\Objective-C class,再点Next。把NSObject作为基类,点Next,并把这个类取名为 GKMatchmakerViewController-LandscapeOnly.m,最后点击Finish。

  把 GKMatchmakerViewController-LandscapeOnly.h 换成下面的代码:

#import   < Foundation / Foundation.h >
#import   < GameKit / GameKit.h >

@interface  GKMatchmakerViewController(LandscapeOnly)
-  (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation;
@end

  然后把 GKMatchmakerViewController-LandscapeOnly.m相应的替换成下面的代码:

#import   " GKMatchmakerViewController-LandscapeOnly.h "

@implementation  GKMatchmakerViewController (LandscapeOnly)

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

@end

  大功告成!编译并运行程序,这时视图控制器显示为横版模式了:

何去何从?

  这里有本教程的完整源代码

  在下部分教程中,我们将会涉及到如何在两台设备之间发送数据,同时把cat Vs kid包装成一个非常好玩的游戏!

  译者的话:前段时间忙着考试,现在又忙着做项目,不好意思,很长一段时间没有更新了,大家见谅。同时,如果翻译过程中有什么明显的错误,请大家给我指出来,谢谢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值