A SpaceManager Game

A SpaceManager Game

 

在学习闲逛中,发现了cocos2d-iphone 官方网站中一篇介绍Chipmunk物理引擎并工具化的教程。在网上搜了下,并无中文翻译,并且Chipmunk物理引擎的相关资料也非常少,于是产生了翻译此篇blog的想法。这几天公司的项目比较忙,距离上一篇基础的SpaceManager翻译文章(The Chipmunk SpaceManager(Chipmunk物理引擎管理类))过了1个多星期这篇才有时间翻译出来。

 

原文相关:

A SpaceManager Game

Published by mobilebros on January 24, 2011 in chipmunk, cocos2d and source code. 73 Comments

Tags: chipmunk, cocos2d, physics, spacemanager, tutorial.

Tweet

这篇文章是建立在之前写的关于SpaceManager一些基础知识的文章之上的,你可以在这里找到它:http://www.cocos2d-iphone.org/archives/677

 

About the Author(关于作者):

我是Rob,我现在正供职于mobile bros. LLC.在最近几年我们已经发布了几个游戏,全都是基于SpaceManager的,包括:Pachingo, Trundle, 和Kill Timmy.本人对于物理相关的非常感兴趣,并经常寻找一些方法把它们融合到我们的游戏中。

 

Goals

今天的任务是使用Cocos2d, Chipmunk, 和SpaceManager设计一个简单的游戏。我相信最好的课程就是从例子中学习。因此这篇文章我将带领你建立一个简单的小游戏,并展示SpaceManager的一些特性。其中的一些特性是:

*保存和加载整个游戏状态

*演示Chipmunk中的爆炸

*创建一个debug-layer

*使用impulses

 

*Retina显示支持

 

那我们该做哪一种类型的游戏呢⋯⋯也许这样一个:我们使用弹弓投掷手榴弹去杀死一些躲在建筑物里面的坏蛋动物?听起来像是一个不错的计划。那么让我们马上开始吧。这个教程使用 Cocos2d 0.99.5版本和SpaceManager 0.0.6版本。我强烈建议你下载这篇文章中附件的源代码,以便你能跟上(理解教程)。这样你可以得到所有(需要的)东西并跳过初始化的过程。资源在这:

Download the Source-Code: GrenadeGame.zip (博主备份:SpaceManager_GrenadeGame.zip)

Edit: Updated source location

Basic SpaceManager Game Setup:

你会创建一个新项目。我将命名我的为GrenadeGame,选择Cocos2d Chipmunk Application 模板。创建好后,跳转到你项目的Finder根目录,把之前你下的SpaceManager游戏例子附件中的“SpaceManager”文件夹放到你的项目根文件夹中⋯⋯或者你项目根目录下的任何地方。

现在返回到Xcode直接添加“SpaceManager”到你的项目中⋯⋯准备地差不多了。为了能够编译通过GrenadeGame,你还必须到XCode的菜单栏点击Project->Edit Active Target “GrenadeGame”,然后找到“Header Search Paths”设置选项,添加一个新值:“$(SRCROOT)/libs/Chipmunk/include/chipmunk”

(译者:xcode4是选择工程,然后再选择Target,在Build Settings的Search Paths里能找到“Header Search Paths”.感觉不用这么麻烦,直接可以拖动丢进xcode中,copy选项打勾,选择Create groups for any added folders,就可以了啊。)

请保持这个引用,否则很可能XCode会出错。你现在应该可以编译并运行了!你应该看到默认的grossini了 。

Doing Some Good Stuff:

现在你的项目全都设置好了(译者:其实还没有全部准备好,不过之后一些文件图片之类的添加省略了,大家可以自己去添加),接着删除HelloWorldScene的.h和.m文件,然后添加一个新的Objective-C类(CCLayer类)。我简单地把它命名为“Game”.之后修改.h文件成这样:

#import "SpaceManagerCocos2d.h"
 
@interface Game : CCLayer
{
     SpaceManagerCocos2d *smgr;
}
+(id) scene; @end .m文件: #import "Game.h" @interface Game (PrivateMethods) @end @implementation Game +(id) scene { CCScene *scene = [CCScene node]; [scene addChild:[Game node]]; return scene; } - (id) init { [super init]; [[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:NO];
//allocate our space manager smgr = [[SpaceManagerCocos2d alloc] init]; smgr.constantDt = 1/55.0;
return self; } - (void) dealloc { [smgr release]; [super dealloc]; } - (BOOL)ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event { return YES; } - (void)ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event { } @end

没有特别疯狂的事发生。我只是初始化了些基本的设置、销毁和处理触摸事件的方法。这时候我想要通过创建一个函数,看见模拟在chipmunk中的shapes 的一些反馈:

- (CCNode*) createBlockAt:(cpVect)pt width:(int)w height:(int)h mass:(int)mass
{
     cpShape *shape = [smgr addRectAt:pt mass:mass width:w height:h rotation:0];
     cpShapeNode *node = [cpShapeNode nodeWithShape:shape];
     node.color = ccc3(56+rand()%200, 56+rand()%200, 56+rand()%200);
     [self addChild:node];
return node; }

你可以看到我刚通过给定的质量创建了一个与cpShapeNode(它会负责绘制一个基本的shape外观)绑定的矩形方块,然后把这个node添加到“self”中,也就是游戏的可见layer中。我在这里使用了rand()函数,以便我在多次调用这个方法的时候得到一些颜色上的变化。现在有了这个函数,我再来修改下init方法,使用这个方法并告诉SpaceManager开始模拟:

[self createBlockAt:cpv(240,200) width:12 height:40 mass:100];
     [smgr start];

我在init方法里分配了smgr的内存之后增加了这两行代码,再运行这个app.啊呀!我的方块在创建后马上就掉出屏幕了。我猜我需要一些容器样的矩形包围住屏幕。在createBlockAt之前添加这个方法:

[smgr addWindowContainmentWithFriction:1.0 elasticity:1.0 inset:cpvzero];

哟!这次运行,这个方块掉下来,弹了几下,就不动了。

这个时候黑色的背景显而易见太简单了。所以我在init的底部使用Paint.NET快速地加载一个图像,并告诉 smgr去添加一些静态段形状(static segment shapes)来表现物理地形。

CCSprite *background = [CCSprite spriteWithFile:@"smgrback.png"];
     background.position = ccp(240,160);
     [self addChild:background];
 
     [smgr addSegmentAtWorldAnchor:cpv(72,13) toWorldAnchor:cpv(480,13) mass:STATIC_MASS radius:1];
     [smgr addSegmentAtWorldAnchor:cpv(72,13) toWorldAnchor:cpv(72,133) mass:STATIC_MASS radius:1];
     [smgr addSegmentAtWorldAnchor:cpv(72,133) toWorldAnchor:cpv(0,133) mass:STATIC_MASS radius:1];

     [self addChild:[smgr createDebugLayer]];

可能你要问createDebugLayer是什么东东?当我没有给shapes绑定任何CCNodes,我就得不到视觉上的反馈,不知道发生了什么事。通过添加一个debug layer可以自动地显示chipmunk中的任何东西,包括大多数的约束(constraints)。这样我可以对那些segments在背景中完美的画线显示出来。

Interactables

我认识到这个init正变的越来越臃肿,但是从现在开始我会分开处理这些事。因为我像先让我的“坏蛋动物”工作起来先!所以我又创建了一个Objective-C文件,然后头文件像是这样:

Ninja.h
#import "SpaceManagerCocos2d.h"

@class Game;
@interface Ninja : cpCCSprite
{
     Game *_game;
     int _damage;
}

+(id) ninjaWithGame:(Game*)game;
-(id) initWithGame:(Game*)game;
-(void) addDamage:(int)damage;
 
@end

cpCCSprite是一个类似于cpShapeNode的CCSprite特殊类型,会保持跟踪对应的shape并与chipmunk做必要的同步。我的计划是使我的坏蛋动物用基本的圆形,我的类定义是这样的:

Ninja.m
#import "Ninja.h"
#import "Game.h"
 
@implementation Ninja

+(id) ninjaWithGame:(Game*)game
{
     return [[[self alloc] initWithGame:game] autorelease];
}

-(id) initWithGame:(Game*)game
{
     cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:50 radius:9];
     [super initWithShape:shape file:@"elephant.png"];
 
     _game = game;

     //Free the shape when we are released
     self.spaceManager = game.spaceManager;
     self.autoFreeShape = YES;
 
     return self;
}
 
-(void) addDamage:(int)damage
{
     _damage += damage;

     if (_damage > 2)
     {
          [_game removeChild:self cleanup:YES];
     }
}

@end

译者:添加到这里可能会报错, 我们需要在Game的.h和.m分别补上下面两句:

@property (readonly) SpaceManager* spaceManager;

@synthesize spaceManager = smgr;

有一些问题留在这里,我现在把它留给你(即译者上面提到的)。让我们开始讨论init方法。首先我创建了一个我们马上要用的shape,一个半径为9的圆。下一步我用这个shape和一个图片初始化了它。我选择了一个大象做为这个坏动物,你可以直接通过文件名使用图片(你如果添加到了工程里面的话)。然后设置autoFreeShape属性为真,同时因此还必须有设置管理这个自动释放过程的spaceManager.我同时也创建了一个addDamage方法。我最基本的要求是能跟踪任何(对坏蛋动物的)“伤害”,并在某个阀值坏蛋动物被“销毁”。

现在我需要一些东西去射这些家伙!我创建了另外一个类似Ninja的类。

Bomb.h:
#import "SpaceManagerCocos2d.h"
 
@class Game;
@interface Bomb : cpCCSprite
{
     Game *_game;
     BOOL _countDown;
}

+(id) bombWithGame:(Game*)game;
-(id) initWithGame:(Game*)game;

-(void) startCountDown;
-(void) blowup;

@end

Bomb.m:

#import "Bomb.h"
#import "Game.h"

@implementation Bomb
 
+(id) bombWithGame:(Game*)game
{
     return [[[self alloc] initWithGame:game] autorelease];
}
 
-(id) initWithGame:(Game*)game
{
     cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:STATIC_MASS radius:7];
     [super initWithShape:shape file:@"bomb.png"];

     _game = game;
     _countDown = NO;

     //Free the shape when we are released
     self.spaceManager = game.spaceManager;
     self.autoFreeShape = YES;

     return self;
}

-(void) startCountDown
{
     //Only start it if we haven't yet
     if (!_countDown)
     {
          _countDown = YES;

          id f1 = [CCFadeTo actionWithDuration:.25 opacity:200];
          id f2 = [CCFadeIn actionWithDuration:.25];
 
          id d = [CCDelayTime actionWithDuration:3];
          id c = [CCCallFunc actionWithTarget:self selector:@selector(blowup)];

          [self runAction:[CCRepeatForever actionWithAction:[CCSequence actions:f1,f2,nil]]];
          [self runAction:[CCSequence actions:d,c,nil]];
     }
}

-(void) blowup
{
     [self.spaceManager applyLinearExplosionAt:self.position radius:100 maxForce:4500];
     [_game removeChild:self cleanup:YES];
}

@end

所以这个init方法应该与之前的类似。然后我已经知道了这个炸弹的功能会是什么,我实现了一个“startCountDown”方法实现炸弹的普通 “闪烁”功能然后在3秒后调用另一个方法“blowup”. “blowup”方法会告诉SpaceManager去控制一个爆炸,并对一个确定的半径内的shapes都产生作用力,然后移除炸弹本身。简单吧!

Collisions

我们差不多开始需要对坏蛋大象发射炸弹,并监测一些碰撞诸如此类。让我们从处理碰撞探测开始。如果你是通过cocos2d的模板创建你的工程,你应该有一个叫做“GameConfig.h”的文件,在“#endif”之前找到一处空地,然后吧下面这些预定义添加进取:

#define kNinjaCollisionType         1
#define kGroundCollisionType        2
#define kBlockCollisionType         3
#define kBombCollisionType          4

这些都是chipmunk需要为了给shapes碰撞定义不同的类型排序的整型值。现在我们回到所有那些我们创建shapes的地方填入他们合适的碰撞类型。比如,Bomb的init方法力需要这行代码:

shape->gt;collision_type = kBombCollisionType;

确保Ninja, Blocks, 还有我们用来做为地面的segments设置合适的碰撞类型。现在我们必须要告诉SpaceManager我们感兴趣的特定碰撞,Ninja w/Ground, Ninja w/Block,等等。把这几行代码放到Game的init方法中:

[smgr addCollisionCallbackBetweenType:kNinjaCollisionType
                          otherType:kGroundCollisionType
                             target:self
                           selector:@selector(handleNinjaCollision:arbiter:space:)
                            moments:COLLISION_POSTSOLVE,nil];
     [smgr addCollisionCallbackBetweenType:kNinjaCollisionType
                          otherType:kBlockCollisionType
                             target:self
                          selector:@selector(handleNinjaCollision:arbiter:space:)
                           moments:COLLISION_POSTSOLVE,nil];
[smgr addCollisionCallbackBetweenType:kNinjaCollisionType otherType:kBombCollisionType target:self selector:@selector(handleNinjaCollision:arbiter:space:) moments:COLLISION_POSTSOLVE,nil]; [smgr addCollisionCallbackBetweenType:kBombCollisionType otherType:kGroundCollisionType target:self selector:@selector(handleBombCollision:arbiter:space:) moments:COLLISION_POSTSOLVE,nil]; [smgr addCollisionCallbackBetweenType:kBombCollisionType otherType:kBlockCollisionType target:self selector:@selector(handleBombCollision:arbiter:space:) moments:COLLISION_POSTSOLVE,nil];

这些回调函数会让SpaceManager在我们特定的碰撞类型之间发生后立即去调用一个ninja的碰撞处理或者一个bomb的碰撞处理。这两个处理方法需要定义在Game中:

-(BOOL) handleNinjaCollision:(CollisionMoment)moment arbiter:(cpArbiter*)arb space:(cpSpace*)space
{
    CP_ARBITER_GET_SHAPES(arb, ninjaShape, otherShape);
 
    //Get a value for "force" generated by collision
    float f = cpvdistsq(ninjaShape->body->v, otherShape->body->v);

    if (f > 600)
    {
     [(Ninja*)ninjaShape->data addDamage:f/600];
    }
   
    //moments:COLLISION_BEGIN, COLLISION_PRESOLVE, COLLISION_POSTSOLVE, COLLISION_SEPARATE  
    return YES;
}

-(BOOL) handleBombCollision:(CollisionMoment)moment arbiter:(cpArbiter*)arb space:(cpSpace*)space
{
    CP_ARBITER_GET_SHAPES(arb, bombShape, otherShape);
   
    [(Bomb*)bombShape->data startCountDown];
    return YES;
}

第一个函数会通过一个以速度为基础计算出假的冲击力给那些坏蛋造成伤害。如果计算的值超过阀值(600看来刚好),那我们就把这个值做为伤害值传递给坏蛋。第二个函数就是当炸弹碰到任何东西的时候激活它的倒计时。碰撞的东西处理完了!

下一步自然是设置一些敌人,然后对它们发射炸弹!在Game的init方法里设置敌人应该是非常简单的事了:     

     Ninja *ninja = [Ninja ninjaWithGame:self];
     ninja.position = ccp(250,23);
     [self addChild:ninja z:5];

炸弹的设置稍微复杂点。我们想要追踪它们,所以我们在Game的头文件中建立了2个成员变量。

////
     NSMutableArray *_bombs;
     Bomb           *_curBomb;
////

然后回到init,我们初始化这个数组,设置我们的炸弹。

     _bombs = [[NSMutableArray array] retain];

     for (int i = 0; i < 3; i++)
     {
          Bomb *bomb = [Bomb bombWithGame:self];
          bomb.position = ccp(10+i*16, 143);
          [self addChild:bomb z:5];
          [_bombs addObject:bomb];
     }

     [self setupNextBomb];

确保把[_bombs release]放在Game的dealloc方法里。我们同样需要设置“当前”的炸弹,所以我们添加这一个方法:

- (void) setupNextBomb
{
     if ([_bombs count])
     {
          _curBomb = [_bombs lastObject];

          //move it into position
          [_curBomb runAction:[CCMoveTo actionWithDuration:.7 position:ccp(60,166)]];
          [_bombs removeLastObject];
     }
     else
          _curBomb = nil;
}

这里我们来看看是否有任意一个炸弹在队列中并至少有一个移动到了指定的位置。我想你们中的一些人想知道我凭什么决定在什么位置使用在这之前的2个代码段(意思就是为什么会这样放置炸弹)。那么接下来的这段代码可能解释一点,它同样是放在Game的init方法中,像这样:

     CCSprite *sling1 = [CCSprite spriteWithFile:@"sling1.png"];
     CCSprite *sling2 = [CCSprite spriteWithFile:@"sling2.png"];
 
     sling1.position = ccp(60,157);
     sling2.position = ccpAdd(sling1.position, ccp(5,10));
 
     [self addChild:sling1 z:10];
     [self addChild:sling2 z:1];

它就是弹弓!我把它放在屏幕边缘的左边中间的位置。“当前”的炸弹放在了弹弓上,其他的炸弹成线排在它的后面。我们真的不需要一个shape与弹弓绑定,它仅仅是2个z orders(层次关系)不同的sprites以便我们“穿过(通过)”它发射炸弹。

要使这个弹弓工作,我们只需要在Game layer实现touch事件函数。

-(BOOL) ccTouchBegan:(UITouch*)touch withEvent:(UIEvent *)event
{
     CGPoint pt = [self convertTouchToNodeSpace:touch];
     float radiusSQ = 25*25;
 
     //Get the vector of the touch
     CGPoint vector = ccpSub(ccp(60,157), pt);

     //Are we close enough to the slingshot?
     if (ccpLengthSQ(vector) < radiusSQ)
          return YES;
     else
          return NO;
}

首先我们实现began方法。我们只想要检测到点击的点是在弹弓的附近。如果在设定的范围内就返回YES,表明我们接受此次触摸事件。接下来是move方法:

-(void) ccTouchMoved:(UITouch*)touch withEvent:(UIEvent *)event
{
    CGPoint pt = [self convertTouchToNodeSpace:touch];
    CGPoint bombPt = ccp(60,166);

    //Get the vector, angle, length, and normal vector of the touch
    CGPoint vector = ccpSub(pt, bombPt);
    CGPoint normalVector = ccpNormalize(vector);
    float angleRads = ccpToAngle(normalVector);
    int angleDegs = (int)CC_RADIANS_TO_DEGREES(angleRads) % 360;
    float length = ccpLength(vector);
   
    //Correct the Angle; we want a positive one
    while (angleDegs < 0)
     angleDegs += 360;
   
    //Limit the length
    if (length > 25)
     length = 25;
   
    //Limit the angle
    if (angleDegs > 245)
     normalVector = ccpForAngle(CC_DEGREES_TO_RADIANS(245));

    else if (angleDegs < 110)
     normalVector = ccpForAngle(CC_DEGREES_TO_RADIANS(110));
   
    //Set the position
     _curBomb.position = ccpAdd(bombPt, ccpMult(normalVector, length));
}

我们通过你手指的移动计算炸弹的位置,同时限制角度和距离弹弓的长度。最后是end方法:

-(void) ccTouchEnded:(UITouch *)touch withEvent:(UIEvent *)event
{
     CGPoint vector = ccpSub(ccp(60,166), _curBomb.position);

     if (_curBomb)
          [smgr morphShapeToActive:_curBomb.shape mass:30];

     [_curBomb applyImpulse:cpvmult(vector, 240)];
 
     [self setupNextBomb];
}

我们获取炸弹位置和发射点的向量,然后把当前的炸弹的质量从静态(不可移动;译者注:指不可通过Chipmunk的物理模拟自动移动)变成一个质量为30的活动物。最后我们用这个向量做为冲击力来发射这个炸弹,然后移动下一个炸弹到当前的发射位置。试一下吧!

我们现在有了一个本质上能工作的游戏了,你能发射炸弹,有障碍,还有敌人可以被摧毁。(在附件的代码例子中)我还增加了一些修改,比如像当敌人死掉的时候“嘭”爆开的烟雾,还有爆炸时候产生的粒子效果,这些我不会在这讨论,因为这会使这篇已经很长的文章越来越庞大了。

Serialization

所以我们那庞大的init方法首先需要分解下,我建立了4个独立处理所有这些设置的方法。

- (void) setupShapes;          //setup terrain and blocks
- (void) setupEnemies;         //setup the enemies
- (void) setupBackground; //setup background image and slingshot
- (void) setupBombs;      //setup the bombs

我把所有的SpaceManager和碰撞的设置放在init里,但是下面我会修改所有的(像是)“垃圾”看起来更可读些:

     [self setupBackground];

     //Try to load it from file, if not then create from scratch
     if (!([smgr loadSpaceFromUserDocs:"saved_state.xml" delegate:self]))
     {
          [self setupBombs];
          [self setupEnemies];
          [self setupShapes];
     }

     [self setupNextBomb];

如你所见,我让SpaceManager从一个文件中去加载chipmunk space;如果加载失败或者文件不存在,我们就手动创建它。注意,我们给这个加载文件的方法传递一了一个代理,代理就是self.也就是说我们能够捕获一些确定的事件。

-(BOOL) aboutToReadShape:(cpShape*)shape shapeId:(long)id
{
    if (shape->collision_type == kBombCollisionType)
    {
     Bomb *bomb = [Bomb bombWithGame:self shape:shape];
     [self addChild:bomb z:5];

     if (cpBodyGetMass(shape->body) == STATIC_MASS)
          [_bombs addObject:bomb];
    }
    else if (shape->collision_type == kNinjaCollisionType)
    {
     Ninja *ninja = [Ninja ninjaWithGame:self shape:shape];
     [self addChild:ninja z:5];
    }
    else if (shape->collision_type == kBlockCollisionType)
    {
     cpShapeNode *node = [cpShapeNode nodeWithShape:shape];
     node.color = ccc3(56+rand()%200, 56+rand()%200, 56+rand()%200);
     [self addChild:node z:5];
    }

    //This just means accept the reading of this shape
    return YES;
}

我们现在只关注读取这些shapes,我实现了这个方法:只通过碰撞类型分辨shapes。一个STATIC_MASS碰撞类型意味着一个炸弹在队列中,所以我们把它加入(炸弹)数组中。你可能注意到了炸弹和坏蛋调用的方法我们还没写好…因为它们都差不多,我就只给出你炸弹的实现方法:

+(id) bombWithGame:(Game*)game shape:(cpShape*)shape
{
     return [[[self alloc] initWithGame:game shape:shape] autorelease];
}

-(id) initWithGame:(Game*)game
{
     cpShape *shape = [game.spaceManager addCircleAt:cpvzero mass:STATIC_MASS radius:7];
     return [self initWithGame:game shape:shape];
}

-(id) initWithGame:(Game*)game shape:(cpShape*)shape;
{
…

那么init方法就得到了一个shape,但是正规老的init方法在被调用的时候会创建一个shape,这一个会让读取的代码工作…我忘记了什么吗?哦,对了我需要一个保存的方法。

-(void)save
{
     [smgr saveSpaceToUserDocs:"saved_state.xml" delegate:self];
}

瞧!我可以在任何时候调用这个方法并且保存当前的状态,init方法会重新加载它,如果存在的话。

Retina Mode

这是一个难题。在程序的代理中,我通常是找到开启FPS显示的那行代码下添加这一条:

 [director enableRetinaDisplay:YES];

就是它了!SpaceManager和Chipmunk并不关心像素的变化,因为对应的点的大小并没有的改变(意思就是这2个工具只关心点,而不关心pixel)。当然你要确定原图两倍大的高清图片资源带有“-hd”的后缀。然后你可能留意到在模拟起中游戏运行起来有点满(30 FPS),因为我们在SpaceManager里用了一个恒定的刷新(constant dt)模式,它会使模拟看起来比较慢。

Closing Notes

包含了例子的源文件比我们这篇文章讨论的添加了更多一点点的内容。增加的这些东西会使这个游戏完成度和可玩性更高。这是一份列表:

-添加了一些块函数去建造柱状建筑和三角建筑(屋顶)

-添加了一些可自定的位置和半径

-跟踪敌人的数量,以便出现在所有敌人被杀光后出现“You Win”字样。

-给坏蛋和炸弹添加“poof”(嘭)的云和粒子效果实现特定的效果。

一些其他的内容可以纪录下来添加进去或者更改:

-一种加载不同关卡的方法(可能通过XML文件?)

-不同的炸弹和坏蛋类型

-弹弓和触碰代码更好地封装起来。

检查一遍示例资源, here’s a video of it in action!

posted @ 2012-05-24 15:25  馒不头  阅读(327)  评论(0编辑  收藏  举报