如何用Cocos2d来开发简单的iPhone游戏教程

这一周接触到Cocos2D开发,在它的官网上看到Ray Wenderlic写的关于cocos2d开发的文章,感觉写的挺好,翻译了一下。
    原文链接地址大家可以在上面看到作者的更多内容

    初次翻译文章,望各位关照,想说的话在作者的文章里边也有表述,就直接开始吧


游戏截图





例子下载:



Cocos2DSimpleGame.zip (776 K) 下载次数:348

      Cocos2D是iPhone开发中一个非常有用的库,它可以让你在创建自己的iPhone游戏时节省很多的时间。它具有很多的功能,比如sprite(精灵)扶持,非常酷的图形效果,动画效果,物理库,音频引擎等等。

      我是一个Cocos2D开发的新手,尽管有很多有用的教程来介绍如何开始利用Cocos2D开发,但我不能找到一个教程是我期待的那样,它可以创建一个简单但功能丰富的游戏,这个游戏具有动画,碰撞还有音频,不需要其它更多的高级功能。我最终自己完成了一个简单的例子,并且在我自己的经验下写了这篇教程以便于它对于其它的新手会有用。

     这篇教程将带你从头到尾的来了解用Cocos2D来创建一个简单的iPhone游戏的过程。你可以一步步的按教程来,也可能跳过直接从文章的最后来下载例子工程。当然,里边会有ninjas(忍者)

下载与安装Cocos2D




    你可以从 the Cocos2D google Code page 下载Cocos2D,现在的最新版本是0.99.0-final(这也是这篇教程使用的)。


    在你下载完代码后,你应该安装有用的工程模板。打开Terminal window(终端窗口),找到下载的Cocos2D所在的目录,输入下面的命令:./install_template.sh。
如果你的XCode不是安装在默认的目录下面(比如说你的机器上面可能安装了多个版本的SDK),你可以在安装脚本里边手工的添加一个参数。(译者注,我没试过,试过的大大可以给指明一下,17楼写明了, 谢谢17楼的提示)


Hello, Cocos2D




      让我们开始来用刚刚安装的Cocos2D工程模板来建立并运行一个简单的Hello World 工程。启动XCode ,选中 cocos2d-0.99.0 Applications模板创建一个新的Cocos2D工程,给工程命名为“Cocos2DSimpleGame

”. 


继续编译并运行该工程。如果一切正常,你将看到下图:



     Cocos2D被组织到”scenes”(场景)的概念中,有点类似于游戏中的”levels”(等级)或是”screens”(屏幕).比如你需要有一个场景来为游戏初始化菜单,一个场景为游戏的主要动作,一个场景为游戏结束。

     在场景里边,你要有许多的图层(就像Photoshop里边的一样),图层可能包含多个(nodes)结点,比如sprites(精灵),labels(标签),menus(菜单)及其它。当然结点也包含其它的结点(比如,一个精灵可以有一个子精灵)。

     在这个例子工程中,你可以看到有一个场景-HelloWorldScene,我们也将在它里边开始实现我们的游戏。继续打开源文件,你会看到在init这个方法中,它加入了一个label来在场景中显示”Hello World”。我们将要放入一个精灵来代替它。
 

添加一个精灵




     在添加精灵之前,我们需要即将用到的图片。你可以自己创建,或者是用Ray Wenderlich妻子为这个工程专门绘制的图片:
a player Image

a Projectile Image

a Target Image


     当你得到这些图片后,把它们直接拖到XCode里边的resources文件夹里边去,一定要选中"Copy items into destination group’s folder (if needed)”。


    既然我们已经有了自己的图片,我们就要找出应该在哪来放置玩家。请注意,在Cocos2D里边屏幕的左下角是坐标原点(0,0),x和y值向右上角递增。因为工程是在横向模式,这意味着右上角的坐标值是(480, 320)。

     还需要注意的是在默认状态下当我们为一个物体设置position属性时,position属性是和我们添加的精灵的中心点关联起来的。因此如果我们想把玩家精灵放置在屏幕水平方向的左边,垂直方向的中间:
position的X坐标,要设置成[player sprite's width]/2.
Position的Y坐标,要设置成[window height]/2
下面这张图可以帮助我们更好的理解



     让我们试一下吧!打开Classes文件夹选中HelloWorldScene.m,用下面的代码来代替init方法:

Cpp代码
  1. -(id) init{  
  2.     if( (self=[super init] )) { 
  3.   
  4.         CGSize winSize = [[CCDirector sharedDirector] winSize];  
  5.         CCSprite *player = [CCSprite spriteWithFile:@"Player.png"  
  6.         rect:CGRectMake(0, 0, 27, 40)];  
  7.         player.position = ccp(player.contentSize.width/2,  
  8.                                           winSize.height/2);  
  9.        [self addChild:player];  
  10.  
  11.   }  
  12.        return self;  
  13. }  



     你现在可以编译并运行这个工程,你的精灵应该会正确显示,但背景默认是黑色的。对这个作品来说,白色背景会更好。在Cocos2D中,把一个图层的的背景颜色更改成为一个自定义颜色的简单方法是利用CCColoredLayer这个类。来尝试一下吧。选中HelloWorldScene.h并且改变HelloWorld接口省明像下面的那样:

Cpp代码
  1. @interface HelloWorld : CCColorLayer  


   然后选中HelloWorldScene.m并对init方法进行一个细微的修改来把背景色改为白色。

Cpp代码
  1. if( (self=[super initWithColor:ccc4(255,255,255,255)] ))  


      继续编译并运行工程,你会看到你的玩家精灵在白色的背景上。噢,我们的忍者已经准备表演了。   


移动目标




     下面我们需要在场景中添加一些目标让忍者去打击。为了让事情变的更有趣一些,我们要让这些目标移动起来-要不然没什么挑战性。我们在稍稍偏屏幕右边的地方创建一些目标,并为它们建立动作来让它们向左移动。

在init方法之前添加下面的方法:

Cpp代码
  1. -(void)addTarget {  
  2.  
  3. CCSprite *target = [CCSprite spriteWithFile:@"Target.png"rect:CGRectMak(0, 0, 27, 40)];  
  4.  
  5. // Determine where to spawn the target along the Y axis  
  6. CGSize winSize = [[CCDirector sharedDirector] winSize];  
  7.  
  8. int minY = target.contentSize.height/2;  
  9. int maxY = winSize.height - target.contentSize.height/2;  
  10. int rangeY = maxY - minY;  
  11. int actualY = (arc4random() % rangeY) + minY;  
  12.  
  13. // Create the target slightly off-screen along the right EDGE,  
  14. // and along a random position along the Y axis as calculated above 
  15.   
  16. target.position = ccp(winSize.width + (target.contentSize.width/2), actualY);  
  17.  
  18. [self addChild:target];  
  19.  
  20. // Determine speed of the target  
  21. int minDuration = 2.0;  
  22. int maxDuration = 4.0;  
  23. int rangeDuration = maxDuration - minDuration;  
  24. int actualDuration = (arc4random() % rangeDuration) + minDuration;  
  25.  
  26. // Create the actions  
  27. id actionMove = [CCMoveTo actionWithDuration:actualDuration position:ccp(-target.contentSize.width/2, actualY)];  
  28.  
  29. id actionMoveDone = [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)];  
  30.  
  31. [target runAction:[CCSequence actions:actionMove,actionMoveDone, nil]];  
  32.  
  33. }  



     在这里我以一种详细的方式来阐明事情以便让事情更容易理解。第一部分我们应该理解目前已经讨论了:我们做一些简单的计算,以确定我们要创建对象,设置对象的位置,并把它以添加玩家精灵的相同方式添加到场景中去。

     这里边的新元素是添加动作。Cocos2D提供了很多非常方便内置的行动可以用来制作动画的行动,如移动,跳跃的行动,褪色的行动,动画动作及更多。在这里我们为目标使用了三项动作。


      •CCMoveTo:我们使用CCMoveTo动作来指导物体屏幕左边。请注意,我们可以指定运动应采取的持续时间,在这里,我们采用2-4秒的随机速度。


      •CCCallFuncN: 该CCCallFuncN函数允许我们指定一个回调到我们的对象出现时,执行操作。

      我们正在指定一个回调称为"spriteMoveFinished”我们还没有写呢


      •CCSequence: 该CCSequence动作让我们创建一系列的动作,一次一个。这样,我们可以先执行CCMoveTo动作,一旦完成执行CCCallFuncN动作。

    下面,添加前面我们已经在CCCallFuncN动作中已经提过的回调函数。你可以在addTarget函数前面添加:

Cpp代码
  1. -(void)spriteMoveFinished:(id)sender {  
  2.  
  3.       CCSprite *sprite = (CCSprite *)sender;  
  4.       [self removeChild:sprite cleanup:YES];  
  5.  

 

      该函数的目的是从场景中移除精灵,一旦该精灵离开屏幕。这一点很重要,这样我们不会随着时间的推移,有许许多多的无用精灵在场景之外而内存泄漏。请注意,还有其他(更好)的方式来解决这个问题诸如具有可重复使用Sprite的数组,但这个初级教程,我们正在采取简单的方法。

      最后一件事情在我们运行程序前。我们需要实际调用的方法来创建目标!为了让事情更有趣点,我们让目标随着时间的推移持续大量的出现。我们可以在Cocos2D中通过安排一个回调函数的定期调用来完成这个任务。每1秒执行一次。因此,在init函数返回之前调用下面的函数调用。

Cpp代码
  1. [self schedule:@selector(gameLogic:) interval:1.0];  


现在像下面这样简单的实现这个回调函数:

Cpp代码
  1. -(void)gameLogic:(ccTime)dt {  
  2.      [self addTarget];  
  3. }  

 


      就是这样!所以,现在,如果你编译并运行该项目,现在你应该看到目标愉快地在屏幕上移动:


射击子弹




      在这时,忍者希望有一些动作-让们添加射击吧!我们有很多的方法来实现射击,但在这个游戏中我们要在用户点击屏幕时来进行射击,从玩家射出的子弹将按照点击的方向前进。

      我想用一个CCMoveTo动作去保持事情还在初级层面上,但为了实现这个,我们必须做一些数学。这是因为CCMoveTo要求我们必须为子弹目的地,但我们不能只使用触摸点,因为接触点仅仅代表相对于玩家的射击方向。事实上,我们希望保持子弹通过触摸点,直到移出屏幕。
用一张图来解释这个事情:



因此,大家可以看到,我们利用起点到触摸点的X和Y方向的偏移创造了个小三角形。我们只需要以同样的比例大三角形 - 我们知道我们需要一个离开屏幕的结束点。
 
好了,上代码。首先,我们必须使我们的层可以支持触摸。添加下面一行到您的init方法:

Cpp代码
  1. self.isTouchEnabled = YES;  


      因为我们已经让图层支持触摸,现在我们可以收到触摸事件的回调。因此,让我们实现ccTouchesEnded方法,只要用户完成了接触该方法就会调用,具体如下:

Cpp代码
  1. - (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {  
  2.  
  3. // Choose one of the touches to work with  
  4. UITouch *touch = [touches anyObject];  
  5. CGPoint location = [touch locationInView:[touch view]];  
  6. location = [[CCDirector sharedDirector] convertToGL:location];  
  7.  
  8. // Set up initial location of projectile  
  9. CGSize winSize = [[CCDirector sharedDirector] winSize];  
  10. CCSprite *projectile = [CCSprite spriteWithFile:@"Projectile.png" rect:CGRectMake(0, 0, 20, 20)];  
  11. projectile.position = ccp(20, winSize.height/2);  
  12.  
  13. // Determine offset of location to projectile  
  14. int offX = location.x - projectile.position.x;  
  15. int offY = location.y - projectile.position.y;  
  16.  
  17. // Bail out if we are shooting down or backwards  
  18. if (offX <= 0) return;  
  19.  
  20. // Ok to add now - we've double checked position  
  21. [self addChild:projectile];  
  22.  
  23. // Determine where we wish to shoot the projectile to  
  24. int realX = winSize.width + (projectile.contentSize.width/2);  
  25. float ratio = (float) offY / (float) offX;  
  26. int realY = (realX * ratio) + projectile.position.y;  
  27. CGPoint realDest = ccp(realX, realY);  
  28.  
  29. // Determine the length of how far we're shooting  
  30. int offRealX = realX - projectile.position.x;  
  31. int offRealY = realY - projectile.position.y;  
  32. float length = sqrtf((offRealX*offRealX)+(offRealY*offRealY));  
  33. float velocity = 480/1; // 480pixels/1sec  
  34. float realMoveDuration = length/velocity;  
  35.  
  36. // Move projectile to actual endpoint  
  37. [projectile runAction:[CCSequence actions:  
  38. [CCMoveTo actionWithDuration:realMoveDuration position:realDest],  
  39. [CCCallFuncN actionWithTarget:self selector:@selector(spriteMoveFinished:)], nil]];  
  40.  
  41. }  


      在第一部分,我们选择一个触摸点来使用,先得到在当前视图中的位置,然后调用convertToGL来把坐标转化到当前的布局。这点很重要,因为我们现在是横向模式。

     接下来我们加载子弹精灵并像往常一样设置初始坐标。然后,我们确定我们希望子弹移动的位置,按照前面描述的算法,使用玩家和触摸点之间的联系来作为指导载体。

     请注意,该算法并不理想。我们正在迫使子弹继续前进直到在X方向上离开屏幕-即使首先在Y方向上已经离开了屏幕!有不同的方法来解决这个,包括检查离开屏幕的最小长度,让游戏的逻辑回调核查离开屏幕的子弹和消除回调,而不是使用回调方法,等等。但这个初级教程,我们将保持原样。

      我们最需要做的就是确定为运动时间。我们希望,子弹会于一个恒定的速率按照射击方向前进,所以我们再次做一些数学。我们可以找出利用勾股定理来算出移动的距离。记得在几何学里边,这是规则,指出了一个直角三角形的斜边长度等于两直角边的平方的和的开方。

     一旦我们有了距离,我们只是除以速度,以获得所需的时间。这是因为速度=距离/时间,或换句话说 时间=距离/速度。

    剩下的事情就是设置动作,就想给目标设置动作一样。编译并运行,现在你的忍者可以向前来的一大群目标开火了 !



碰撞检测




     所以现在我们已经让shurikens满天飞了-但我们的忍者真正想要做的是放倒一些目标。因此,让我们加入一些代码,以检测当我们的子弹和目标的相交。

     要做到这一点,我们首先要在目前的场景中更好的跟踪目标和子弹。把以下的代码添加到你的HelloWorldScene类声明中:

Cpp代码
  1. NSMutableArray *_targets;  
  2. NSMutableArray *_projectiles;  


在init方法中初始化这两个数组:

Cpp代码
  1. _targets = [[NSMutableArray alloc] init];  
  2. _projectiles = [[NSMutableArray alloc] init];  


我们也应该考虑,在你的dealloc方法中清理内存:

Cpp代码
  1. [ _targets release];  
  2. _targets = nil;  
  3. [ _projectiles release];  
  4. _projectiles = nil;  


      现在,修改你的addTarget方法,添加新目标到目标数组中并给它设置一个标记以便在以后使用:

Cpp代码
  1. target.tag = 1;  
  2. [ _targets addObject:target];  


   

     还要修改你的ccTouchesEnded方法,把新子弹添加到子弹数组中给它设置一个标记以便在以后使用:

Cpp代码
  1. projectile.tag = 2;  
  2. [_projectiles addObject:projectile]; 


     最后,修改你的spriteMoveFinished方法,根据标记的不同在适当的数组中移除精灵:

Java代码
  1. if (sprite.tag == 1) {  
  2.           // target  
  3.           [_targets removeObject:sprite];  
  4.  
  5. } else  if (sprite.tag == 2) {  
  6.           // projectile  
  7.           [_projectiles removeObject:sprite];  


      编译并运行该项目以确保一切正常。在这个时候应该没有什么明显的不同,但现在我们有标记,我们要实现碰撞检测。


现在在HelloWorldScene类中添加下面的方法:

Cpp代码
  1. - (void)update:(ccTime)dt {  
  2.       NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];  
  3.  
  4. for (CCSprite *projectile in _projectiles) {  
  5.  
  6.        CGRect projectileRect = CGRectMake( projectile.position.x -(projectile.contentSize.width/2), projectile.position.y - (projectile.contentSize.height/2), projectile.contentSize.width, projectile.contentSize.height);  
  7.  
  8. NSMutableArray *targetsToDelete = [[NSMutableArray alloc] init];  
  9.  
  10. for (CCSprite *target in _targets) {  
  11.  
  12.     CGRect targetRect = CGRectMake( target.position.x -(target.contentSize.width/2), target.position.y - (target.contentSize.height/2), target.contentSize.width, target.contentSize.height);  
  13.  
  14. if (CGRectIntersectsRect(projectileRect, targetRect)) {  
  15.  
  16.         [targetsToDelete addObject:target];  
  17.  
  18.   
  19. }  
  20.  
  21. for (CCSprite *target in targetsToDelete) {  
  22.         [_targets removeObject:target];  
  23.         [self removeChild:target cleanup:YES];  
  24. }  
  25.  
  26. if (targetsToDelete.count > 0) {  
  27.        [projectilesToDelete addObject:projectile]; 
  28. }  
  29.  
  30. [targetsToDelete release];  
  31.  
  32. }  
  33.  
  34.  
  35. for (CCSprite *projectile in projectilesToDelete) {  
  36.           [_projectiles removeObject:projectile];  
  37.           [self removeChild:projectile cleanup:YES]; 
  38. }  
  39.  
  40. [projectilesToDelete release];  
  41.  
  42. }  


    以上应该很清楚了。我们只是通过子弹和目标数组,按照它们的边界框创建相应的矩形,并使用CGRectIntersectsRect方法来检查交叉。

    如果发现有,我们从场景和数组中把它们移除。请注意,我们是把这些对象添加到一个toDelete数组中,因为你不能在一个正在迭代的数组中删除一个对象。同样,有更多的最佳方法来实现这种事情,但我采用了这个简单的方法


      在你准备要运行前你只需要做一件事-通过添加下面的代码到init方法中去安排上面的方法尽可能多的运行

  

Cpp代码
  1. [self schedule:@selector(update:)];  


让它编译并运行,现在当你的子弹和目标碰撞时它们就会消失!



最后的润色




      我们非常接近拥有一个可行的(但非常简单)的游戏了。我们只需要添加一些声音效果和音乐(因为什么类型的游戏没有音乐的!)和一些简单的游戏逻辑。

     如果您一直关注我的blog series on audio programming for the iPhoneblog series on audio programming for the iPhone,关于iPhone的一系列音频编程博客,你会非常高兴地知道,对于Cocos2D开发者来说,在游戏中实现基本的声音效果是多么的简单。


第一步:

       拖动一些背景音乐和一个射击声音效果到你的resources文件夹中。随意使用
cool background music I made background-music-aac.caf.zip (252 K) 下载次数:52
或是 awesome pew-pew sound effect pew-pew-lei.caf.zip (40 K) 下载次数:42 ,或者制作你自己的。

然后:

       添加下面的代码到你的HelloWorldScene.m文件的头部:

Cpp代码
  1. #import "SimpleAudioEngine.h"  


     在你的init方法,像下面的代码所示启动背景音乐:

Cpp代码
  1. [[SimpleAudioEngine sharedEngine]playBackgroundMusic:@"background-music-aac.caf"];  


0.99-final update:(关于0.99-final更新):

       看起来在0.99-final版本中有一个小小的bug,背景音乐只能播放一次(而它本应该循环)-要么是它的错要么就是我弄错了。对于一个变通方法,请参阅本文结尾的意见。
关于  0.99-final版本中有一个小小的bug,

在 CDAudioManager.m 的第72行加入以下代码,  可以解决背景音乐只能播放一次(而它本应该循环)

 - (void)setNumberOfLoops:(NSInteger)theNumberOfLoops {    numberOfLoops = theNumberOfLoops;    audioSourcePlayer.numberOfLoops = theNumberOfLoops;} 



在你的ccTouchesEnded方法中播放下面的声音效果:

Cpp代码
  1. [[SimpleAudioEngine sharedEngine] playEffect:@"pew-pew-lei.caf"];  


      现在,让我们创建一个新的场景,将作为我们的“你赢了”,或“你输”的指示。点击Classes文件夹,进入File\New File,并选择Objective-C class,并确定了NSObject类被选中。单击Next,然后输入GameOverScene作为文件名,并确保“Also create GameOverScene.h”被选中。

然后用下面的代码来代替GameOverScene.h中的内容:

Cpp代码
  1. #import "cocos2d.h"  
  2.  
  3. @interface GameOverLayer : CCColorLayer {  
  4.  
  5.     CCLabel *_label;  
  6.  
  7. }  
  8. @property (nonatomic, retain) CCLabel *label;  
  9.  
  10. @end  
  11.  
  12.  
  13. @interface GameOverScene : CCScene {  
  14.     GameOverLayer *_layer;  
  15. }  
  16. @property (nonatomic, retain) GameOverLayer *layer;  
  17. @end  


再用下面的代码来代替GameOverScene.m中的内容

Cpp代码
  1. #import "GameOverScene.h"  
  2. #import "HelloWorldScene.h"  
  3.  
  4. @implementation GameOverScene  
  5. @synthesize layer = _layer;  
  6.  
  7. - (id)init {  
  8.  
  9. if ((self = [super init])) {  
  10. self.layer = [GameOverLayer node];  
  11. [self addChild:_layer];  
  12.  
  13. }  
  14. return self;  
  15.  
  16. }  
  17.  
  18. - (void)dealloc {  
  19.       [ _layer release];  
  20.         _layer = nil;  
  21.       [super dealloc];  
  22. }  
  23.  
  24. @end  
  25.  
  26. @implementation GameOverLayer  
  27.  
  28. @synthesize label = _label;  
  29.  
  30. -(id) init {  
  31.  
  32. if( (self=[super initWithColor:ccc4(255,255,255,255)] )) {  
  33.  
  34. CGSize winSize = [[CCDirector sharedDirector] winSize];  
  35. self.label = [CCLabel labelWithString:@"" fontName:@"Arial" fontSize:32];  
  36. _label.color = ccc3(0,0,0);  
  37. _label.position = ccp(winSize.width/2, winSize.height/2);  
  38. [self addChild:_label];  
  39.  
  40. [self runAction:[CCSequence actions: [CCDelayTime actionWithDuration:3], [CCCallFunc actionWithTarget:self selector:@selector(gameOverDone)], nil]];  
  41.  
  42. }  
  43.     return self;  
  44.  
  45. }  
  46.  
  47. - (void)gameOverDone {  
  48.  
  49. [[CCDirector sharedDirector] replaceScene:[HelloWorld scene]];  
  50.  
  51. }  
  52.  
  53. - (void)dealloc {  
  54.           [_label release];  
  55.            _label = nil;  
  56.           [super dealloc];  
  57. }  
  58.  
  59. @end  



      请注意,这里有两个不同的对象:一个场景(scene)和一个图层(layer)。场景可以包含多个图层,尽管在这个例子是它只有一个。图层里边只在屏幕中心放了一个标签,并安排了一个3秒中的过渡,然后返回到HelloWorld场景中。


      最后,让我们添加一些非常基本的游戏逻辑。首先,让我们跟踪玩家破坏的目标。添加一个成员变量到您的HelloWorldScene.h中 HelloWorld类如下:

Cpp代码
  1. int _projectilesDestroyed;   


在HelloWorldScene.m中,添加GameOverScene类的声明:

Cpp代码
  1. #import "GameOverScene.h" 

 


     在update方法中removeChile:target:后面的targetsToDelete循环中增加计数并检查获胜条件

Cpp代码
  1. _projectilesDestroyed++;  
  2.  
  3. if (_projectilesDestroyed > 30) {  
  4.  
  5. GameOverScene *gameOverScene = [GameOverScene node];  
  6. [gameOverScene.layer.label setString:@"You Win!"];  
  7. [[CCDirector sharedDirector] replaceScene:gameOverScene];  
  8.  
  9. }  

     最后我们这样来规定,即使只有一个目标过去了,你就输了。修改spriteMoveFinished方法,在removeChild:sprite:方法的后面的tag == 1条件里边添加下面的代码:

Java代码
  1. GameOverScene *gameOverScene = [GameOverScene node];  
  2. [gameOverScene.layer.label setString:@"You Lose :["];  
  3. [[CCDirector sharedDirector] replaceScene:gameOverScene];  



继续编译并运行该项目,这样你有了羸和输的判断条件并会在适当的时候看到游戏结束的场景。

发布了44 篇原创文章 · 获赞 124 · 访问量 185万+
展开阅读全文

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

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览