cocos2d-x與精靈共舞
原文地址:原文
譯者:晉文格墨
注:譯者水平有限,歡迎大家批評斧正。
Cocos2d是一個具有豐富的圖形處理API,它可以使遊戲開發者可以輕鬆地使用廣泛的功能。本文將討論一下sprites的基本用法。
繪製sprites
在2D遊戲開發中最基本的任務就是繪製一個sprite。在這個領域裏,cocosd給用戶提供了很多的靈活性。在本小節裏,我們將介紹如何使用CCSprite, spritesheets, CCSpriteFrameCache, 和 CCSpriteBatchNode來繪製sprite。我們也會再次熟悉一下mipmaping。本節中我們會看到《愛麗絲鏡中奇遇》中的一個場景。
準備
請參考此工程 RecipeCollection01,它包含了本節的所有代碼。
繼續….
執行如下代碼:
@implementation Ch1_DrawingSprites
-(CCLayer*) runRecipe {
/*** Draw a sprite using CCSprite ***/
CCSprite *tree1 = [CCSprite spriteWithFile:@"tree.png"];
//Position the sprite using the tree base as a guide (y anchor
point = 0)
[tree1 setPosition:ccp(20,20)];
tree1.anchorPoint = ccp(0.5f,0);
[tree1 setScale:1.5f];
[self addChild:tree1 z:2 tag:TAG_TREE_SPRITE_1];
/*** Load a set of spriteframes from a PLIST file and draw one by
name ***/
//Get the sprite frame cache singleton
CCSpriteFrameCache *cache = [CCSpriteFrameCache
sharedSpriteFrameCache];
//Load our scene sprites from a spritesheet
[cache addSpriteFramesWithFile:@"alice_scene_sheet.plist"];
//Specify the sprite frame and load it into a CCSprite
CCSprite *alice = [CCSprite spriteWithSpriteFrameName:@"alice.png"];
//Generate Mip Maps for the sprite
[alice.texture generateMipmap];
ccTexParams texParams = { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR, GL_
CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE };
[alice.texture setTexParameters:&texParams];
//Set other information.
[alice setPosition:ccp(120,20)];
[alice setScale:0.4f];
alice.anchorPoint = ccp(0.5f,0);
//Add Alice with a zOrder of 2 so she appears in front of other
sprites
[self addChild:alice z:2 tag:TAG_ALICE_SPRITE];
//Make Alice grow and shrink.
[alice runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCScaleTo actionWithDuration:4.0f scale
:0.7f], [CCScaleTo actionWithDuration:4.0f scale:0.1f], nil] ] ];
/*** Draw a sprite CGImageRef ***/
UIImage *uiImage = [UIImage imageNamed: @"cheshire_cat.png"];
CGImageRef imageRef = [uiImage CGImage];
CCSprite *cat = [CCSprite spriteWithCGImage:imageRef key:@
"cheshire_cat.png"];
[cat setPosition:ccp(250,180)];
[cat setScale:0.4f];
[self addChild:cat z:3 tag:TAG_CAT_SPRITE];
/*** Draw a sprite using CCTexture2D ***/
CCTexture2D *texture = [[CCTextureCache sharedTextureCache]
addImage:@”tree.png”];
CCSprite *tree2 = [CCSprite spriteWithTexture:texture];
[tree2 setPosition:ccp(300,20)];
tree2.anchorPoint = ccp(0.5f,0);
[tree2 setScale:2.0f];
[self addChild:tree2 z:2 tag:TAG_TREE_SPRITE_2];
/*** Draw a sprite using CCSpriteFrameCache and CCTexture2D ***/
CCSpriteFrame *frame = [CCSpriteFrame frameWithTexture:texture
rect:tree2.textureRect];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFrame:
frame name:@”tree.png”];
CCSprite *tree3 = [CCSprite spriteWithSpriteFrame:[[CCSpriteFrame
Cache sharedSpriteFrameCache] spriteFrameByName:@”tree.png”]];
[tree3 setPosition:ccp(400,20)];
tree3.anchorPoint = ccp(0.5f,0);
[tree3 setScale:1.25f];
[self addChild:tree3 z:2 tag:TAG_TREE_SPRITE_3];
/*** Draw sprites using CCBatchSpriteNode ***/
//Clouds
CCSpriteBatchNode *cloudBatch = [CCSpriteBatchNode
batchNodeWithFile:@"cloud_01.png" capacity:10];
[self addChild:cloudBatch z:1 tag:TAG_CLOUD_BATCH];
for(int x=0; x CCSprite *s = [CCSprite spriteWithBatchNode:cloudBatch
rect:CGRectMake(0,0,64,64)];
[s setOpacity:100];
[cloudBatch addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%150+200)];
}
//Middleground Grass
int capacity = 10;
CCSpriteBatchNode *grassBatch1 = [CCSpriteBatchNode
batchNodeWithFile:@"grass_01.png" capacity:capacity];
[self addChild:grassBatch1 z:1 tag:TAG_GRASS_BATCH_1];
for(int x=0; x CCSprite *s = [CCSprite spriteWithBatchNode:grassBatch1
rect:CGRectMake(0,0,64,64)];
[s setOpacity:255];
[grassBatch1 addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%20+70)];
}
//Foreground Grass
CCSpriteBatchNode *grassBatch2 = [CCSpriteBatchNode
batchNodeWithFile:@"grass_01.png" capacity:10];
[self addChild:grassBatch2 z:3 tag:TAG_GRASS_BATCH_2];
for(int x=0; x CCSprite *s = [CCSprite spriteWithBatchNode:grassBatch2
rect:CGRectMake(0,0,64,64)];
[s setOpacity:255];
[grassBatch2 addChild:s];
[s setPosition:ccp(arc4random()%500-50, arc4random()%40-10)];
}
/*** Draw colored rectangles using a 1px x 1px white texture ***/
//Draw the sky using blank.png
[self drawColoredSpriteAt:ccp(240,190) withRect:CGRectMa
ke(0,0,480,260) withColor:ccc3(150,200,200) withZ:0];
//Draw the ground using blank.png
[self drawColoredSpriteAt:ccp(240,30)
withRect:CGRectMake(0,0,480,60) withColor:ccc3(80,50,25) withZ:0];
return self;
}
-(void) drawColoredSpriteAt:(CGPoint)position withRect:(CGRect)rect
withColor:(ccColor3B)color withZ:(float)z {
CCSprite *sprite = [CCSprite spriteWithFile:@"blank.png"];
[sprite setPosition:position];
[sprite setTextureRect:rect];
[sprite setColor:color];
[self addChild:sprite];
//Set Z Order
[self reorderChild:sprite z:z];
}
@end
它是如何工作的….
本節給我們介紹了繪製sprite的一般方法:
從一個文件中創建一個CCSprite:
首先,介紹一些繪製一個sprite的最簡單的方法。這涉及到使用CCSprite類方法,如下:
+(id)spriteWithFile:(NSString*)filename;
這是初始化一個sprite的最直接的方法,並且適合很多種情況。
從一個文件中加載一個sprite的另一種方法:
之後我們將看到使用UIImage/CG/ImageRef,CCTexture2D來創建CCSprite的例子,還有使用一個CCTexture2D對象創建CCSpriteFrame的例子。CGImageRef支持將Cocos2D綁定到其他的框架和工具。CCTexture2D是創建紋理的底層機製。
使用CCSpriteFrameCache加載spritesheet:
下麵,我們將看到使用sprite的最美妙的方法,就是CCSpriteFrameCache類。CCSpriteFrameCache對象是所有sprite幀的緩存,在CocosD-iPhone v0.99中有介紹。使用一個spritesheet和它相關聯的PLIST文件,我們可以加載多個sprite到緩存中。使用下麵的代碼我們可以使用緩存中的sprite創建CCSprite:
+(id)spriteWithSpriteFrameName:(NSString*)filename;
Mipmaping:
Mipmaping允許你縮放一個紋理或者不混淆你的sprites的情況下縮放一個背景。當我們將愛麗絲縮小時,混淆將不能避免地發生。但是開啟Mipmaping之後,Cocos2D將在一個更小區域裏動態地產生低分辨率的紋理來事像素化平滑過渡。返回前麵的代碼,將如下代碼注釋掉:
[alice.texture generateMipmap];
ccTexParams texParams = { GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR,
GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE };
[alice.texture setTexParameters:&texParams];
現在你可看到愛麗絲的像素畫變小了。
使用CCSpriteBatchNode繪製多個sprite副本。
在v0.99.5版本引入的CCSpriteBatchNode類介紹了一個重複繪製同一個sprite對象的高效方方達。這是另一個額外高效的方法,雖然你可以不需要不這樣做。在這三個例子中我們繪製了10個隨機位置的白雲和60個隨機位置的小草。
繪製彩色的矩形。
最後,有一個經常會用的一個簡單技術。通過繪製一個一個空白像素的sprite紋理,然後向其充顏色並設置它的textrueRect屬性,這樣我們就可以創建一個非常有用的色條:
CCSprite *sprite = [CCSprite spriteWithFile:@"blank.png"];
[sprite setTextureRect:CGRectMake(0,0,480,320)];
[sprite setColor:ccc3(255,128,0)];
值這個例子中,我們使用這個技術創建了一個簡單的草地和天空背景。
給sprites上色
在之前的小節中我們使用帶顏色的矩形繪製了簡單的草地和天空背景。能夠設置紋理顏色和透明度的是一個簡單工具,如果巧妙運用的話可以創造出非常酷的效果。在本小節中我們將創建兩個武士各自拿著一個發光武士刀對峙的場景。
準備
請參考RecipeCollection01工程的代碼並注意一些為簡潔而刪除的代碼。
如何做?
執行如下代碼:
//Add a sinister red glow gradient behind the evil samurai
CCGradientLayer *redGradient = [CCGradientLayer
layerWithColor:ccc4(0,0,0,0) toColor:ccc4(255,0,0,100) withDirection
:CCGradientDirectionT_B width:200 height:200];
[redGradient setPosition:ccp(280,60)];
[redGradient setRotation:-90];
[self addChild:redGradient z:2 tag:TAG_RED_GRADIENT];
// Make the swords glow
[self glowAt:ccp(230,280) withScale:CGSizeMake(3.0f, 11.0f)
withColor:ccc3(0,230,255) withRotation:45.0f withSprite:goodSamurai];
[self glowAt:ccp(70,280) withScale:CGSizeMake(3.0f, 11.0f)
withColor:ccc3(255,200,2) withRotation:-45.0f withSprite:evilSamurai];
return self;
}
-(void) initButtons {
[CCMenuItemFont setFontSize:16];
//’Fade To Black’ button
CCMenuItemFont* fadeToBlack = [CCMenuItemFont itemFromString:@
"FADE TO BLACK" target:self selector:@selector(fadeToBlackCallback:)];
CCMenu *fadeToBlackMenu = [CCMenu menuWithItems:fadeToBlack, nil];
fadeToBlackMenu.position = ccp( 180 , 20 );
[self addChild:fadeToBlackMenu z:4 tag:TAG_FADE_TO_BLACK];
}
/* Fade the scene to black */
-(void) fadeToBlackCallback:(id)sender {
CCSprite *fadeSprite = [self getChildByTag:TAG_FADE_SPRITE];
[fadeSprite stopAllActions];
[fadeSprite setColor:ccc3(0,0,0)];
[fadeSprite setOpacity:0.0f];
[fadeSprite runAction:
[CCSequence actions:[CCFadeIn actionWithDuration:2.0f], [CCFadeOut
actionWithDuration:2.0f], nil] ];
}
/* Create a glow effect */
-(void) glowAt:(CGPoint)position withScale:(CGSize)size
withColor:(ccColor3B)color withRotation:(float)rotation
withSprite:(CCSprite*)sprite {
CCSprite *glowSprite = [CCSprite spriteWithFile:@"fire.png"];
[glowSprite setColor:color];
[glowSprite setPosition:position];
[glowSprite setRotation:rotation];
[glowSprite setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCScaleTo actionWithDuration:0.9f
scaleX:size.width scaleY:size.height], [CCScaleTo
actionWithDuration:0.9f scaleX:size.width*0.75f scaleY:size.
height*0.75f], nil] ] ];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCFadeTo actionWithDuration:0.9f
opacity:150], [CCFadeTo actionWithDuration:0.9f opacity:255], nil] ]
];
[sprite addChild:glowSprite];
}
@end
它是如何工作的?
本小節展示了一些基本的色彩處理技術。
設置sprite顏色
設置sprite顏色的最簡單的的方法如下:
-(void) setColor:(ccColor3B)color;
設置sprite顏色有效減少了你可以顯示的顏色,但是它允許在繪製的時候提供更多的編程靈活性。在本節中我們使用setColor設置了很多東西,隻用它畫了一個藍色的天空,一個黃色的太陽,一個黑色的戲劇電影顯示條,ccColor3B是一個C結構,它包含了3個GLubyte變量。使用如下的代碼創建一個ccClor3B結構:
ccColor3B ccc3(const GLubyte r, const GLubyte g, const GLubyte
b);
Cocos2d也定義了一係列的預定義的常量顏色,它們是:
ccWHITE, ccYELLOW, ccBLUE, ccGREEN, ccRED,
ccMAGENTA, ccBLACK, ccORANGE, ccGRAY
過渡到一個顏色
讓場景過渡到一個指定的顏色,我們可以使用一個blank.png技術,在最後我們將介紹它。我們首先繪製一個和場景一樣大的sprite,然後將其填充我們要過渡的目標顏色,然後執行CCFadin動作將sprite過渡到這個顏色。
[fadeSprite setColor:ccc3(255,255,255)];
[fadeSprite setOpacity:0.0f];
[fadeSprite runAction: [CCFadeIn actionWithDuration:2.0f] ];
使用CCGradienLayer
使用CCGradienLayer類我們可以創建漸變效果。讓背景中的山過渡到兩個武士站在的草地,我們可以使用如下代碼來實現漸變過渡:
CCGradientLayer *gradientLayer = [CCGradientLayer layerWithColor
:ccc4(61,33,62,255) toColor:ccc4(65,89,54,255) withDirection:CCGra
dientDirectionT_B width:480 height:100];
[gradientLayer setPosition:ccp(0,50)];
[self addChild:gradientLayer z:0 tag:TAG_GROUND_GRADIENT];
因為CCGradienLayer同樣可以控製透明度,這個功能很有用。正如你看到的,那個邪惡的武士背後是發紅光的。
讓sprite發光:為了讓demo中的刀發光我們使用巧妙的顏色操作,加上混合,調和和縮放動作。首先加載Cocos2d提供的fire.png。通過改變它X和Y方向尺寸,我們可以讓其變瘦或者變胖。如果你想改變其縱橫比(因為例子中的縱橫比是3:11,已經很瘦了),你可以將其縮放的更生動些。你同樣需要設置混合函數{GL_ONE,GL_ONE}添加混合。最後這個sprite的效果添加到真實的sprite上。
CCSprite *glowSprite = [CCSprite spriteWithFile:@"fire.png"];
[glowSprite setColor:color];
[glowSprite setPosition:position];
[glowSprite setRotation:rotation];
[glowSprite setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCScaleTo actionWithDuration:0.9f
scaleX:size.width scaleY:size.height], [CCScaleTo
actionWithDuration:0.9f scaleX:size.width*0.75f scaleY:size.
height*0.75f], nil] ] ];
[glowSprite runAction: [CCRepeatForever actionWithAction:
[CCSequence actions:[CCFadeTo actionWithDuration:0.9f
opacity:150], [CCFadeTo actionWithDuration:0.9f opacity:255], nil]
] ];
[sprite addChild:glowSprite];
sprite的動畫
現在給sprite添加動畫,這裏要強調的是動畫的複雜度和你製作動畫的難度差不多。在本節中,我們將使用簡單的動畫創建一個引人入勝的效果。我們創建一群蝙蝠飛向一個讓人驚悚的城堡。並使用之前介紹的技術添加一個冷色調的光照效果。
開始
請參考RecipeCollection01工程的代碼,並注意遺漏的部分的代碼。
如何做?
執行如下代碼:
//SimpleAnimObject.h
@interface SimpleAnimObject : CCSprite {
int animationType;
CGPoint velocity;
}
@interface Ch1_AnimatingSprites {
NSMutableArray *bats;
CCAnimation *batFlyUp;
CCAnimation *batGlideDown;
CCSprite *lightningBolt;
CCSprite *lightningGlow;
int lightningRemoveCount;
}
-(CCLayer*) runRecipe {
//Add our PLIST to the SpriteFrameCache
[[CCSpriteFrameCache sharedSpriteFrameCache]
addSpriteFramesWithFile:@”simple_bat.plist”];
//Add a lightning bolt
lightningBolt = [CCSprite spriteWithFile:@"lightning_bolt.png"];
[lightningBolt setPosition:ccp(240,160)];
[lightningBolt setOpacity:64];
[lightningBolt retain];
//Add a sprite to make it light up other areas.
lightningGlow = [CCSprite spriteWithFile:@"lightning_glow.png"];
[lightningGlow setColor:ccc3(255,255,0)];
[lightningGlow setPosition:ccp(240,160)];
[lightningGlow setOpacity:100];
[lightningGlow setBlendFunc: (ccBlendFunc) { GL_ONE, GL_ONE }];
[lightningBolt addChild:lightningGlow];
//Set a counter for lightning duration randomization
lightningRemoveCount = 0;
//Bats Array Initialization
bats = [[NSMutableArray alloc] init];
//Add bats using a batch node.
CCSpriteBatchNode *batch1 = [CCSpriteBatchNode
batchNodeWithFile:@"simple_bat.png" capacity:10];
[self addChild:batch1 z:2 tag:TAG_BATS];
//Make them start flying up.
for(int x=0; x //Create SimpleAnimObject of bat
SimpleAnimObject *bat = [SimpleAnimObject
spriteWithBatchNode:batch1 rect:CGRectMake(0,0,48,48)];
[batch1 addChild:bat];
[bat setPosition:ccp(arc4random()%400+40, arc4random()%150+150)];
//Make the bat fly up. Get the animation delay (flappingSpeed).
float flappingSpeed = [self makeBatFlyUp:bat];
//Base y velocity on flappingSpeed.
bat.velocity = ccp((arc4random()%1000)/500 + 0.2f, 0.1f/
flappingSpeed);
//Add a pointer to this bat object to the NSMutableArray
[bats addObject:[NSValue valueWithPointer:bat]];
[bat retain];
//Set the bat’s direction based on x velocity.
if(bat.velocity.x > 0){
bat.flipX = YES;
}
}
//Schedule physics updates
[self schedule:@selector(step:)];
return self;
}
-(float)makeBatFlyUp:(SimpleAnimObject*)bat {
CCSpriteFrameCache * cache = [CCSpriteFrameCache
sharedSpriteFrameCache];
//Randomize animation speed.
float delay = (float)(arc4random()%5+5)/80;
CCAnimation *animation = [[CCAnimation alloc] initWithName:@
“simply_bat_fly” delay:delay];
//Randomize animation frame order.
int num = arc4random()%4+1;
for(int i=1; i [animation addFrame:[cache spriteFrameByName:[NSString
stringWithFormat:@"simple_bat_0%i.png",num]]];
num++;
if(num > 4){ num = 1; }
}
//Stop any running animations and apply this one.
[bat stopAllActions];
[bat runAction:[CCRepeatForever actionWithAction: [CCAnimate
actionWithAnimation:animation]]];
//Keep track of which animation is running.
bat.animationType = BAT_FLYING_UP;
return delay; //We return how fast the bat is flapping.
}
-(void)makeBatGlideDown:(SimpleAnimObject*)bat {
CCSpriteFrameCache * cache = [CCSpriteFrameCache
sharedSpriteFrameCache];
//Apply a simple single frame gliding animation.
CCAnimation *animation = [[CCAnimation alloc] initWithName:@
“simple_bat_glide” delay:100.0f];
[animation addFrame:[cache spriteFrameByName:@"simple_bat_01.png"]];
//Stop any running animations and apply this one.
[bat stopAllActions];
[bat runAction:[CCRepeatForever actionWithAction: [CCAnimate
actionWithAnimation:animation]]];
//Keep track of which animation is running.
bat.animationType = BAT_GLIDING_DOWN;
}
-(void)step:(ccTime)delta {
CGSize s = [[CCDirector sharedDirector] winSize];
for(id key in bats){
//Get SimpleAnimObject out of NSArray of NSValue objects.
SimpleAnimObject *bat = [key pointerValue];
//Make sure bats don’t fly off the screen
if(bat.position.x > s.width){
bat.velocity = ccp(-bat.velocity.x, bat.velocity.y);
bat.flipX = NO;
}else if(bat.position.x bat.velocity = ccp(-bat.velocity.x, bat.velocity.y);
bat.flipX = YES;
}else if(bat.position.y > s.height){
bat.velocity = ccp(bat.velocity.x, -bat.velocity.y);
[self makeBatGlideDown:bat];
}else if(bat.position.y bat.velocity = ccp(bat.velocity.x, -bat.velocity.y);
[self makeBatFlyUp:bat];
}
//Randomly make them fly back up
if(arc4random()%100 == 7){
if(bat.animationType == BAT_GLIDING_DOWN){ [self
makeBatFlyUp:bat]; bat.velocity = ccp(bat.velocity.x, -bat.
velocity.y); }
else if(bat.animationType == BAT_FLYING_UP){ [self
makeBatGlideDown:bat]; bat.velocity = ccp(bat.velocity.x, -bat.
velocity.y); }
}
//Update bat position based on direction
bat.position = ccp(bat.position.x + bat.velocity.x, bat.position.y
+ bat.velocity.y);
}
//Randomly make lightning strike
if(arc4random()%70 == 7){
if(lightningRemoveCount [self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT];
lightningRemoveCount = arc4random()%5+5;
}
}
//Count down
lightningRemoveCount -= 1;
//Clean up any old lightning bolts
if(lightningRemoveCount == 0){
[self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO];
}
}
@end
它是圖和工作的?
本節將介紹使用SImpleAnimObject構造一個基本的動畫類:
動畫對象類的結構:
當一個動畫轉向另一個動畫時,記錄動畫對象的狀態是很重要的一個工作。在本例子中,我們使用SimpleAnimObject,它保存了隨意的animationType的變量。我們同樣保存了一個速度變量,它有一個Y值,此值和動畫幀的延遲成反比:
@interface SimpleAnimObject : CCSprite {
int animationType;
CGPoint velocity;
}
根據動畫係統的深度,你同樣需要保存更多的信息,比如指向一個正在執行的CCAnimation實例的指針,幀信息,和物理形狀。
更多….
當你越來越深入學習Cocos2d遊戲開發,你將會變得越來越傾向於使用assynchronous action來實現遊戲的邏輯和人工智能。從CCAction這個類繼承的類,任何移動的動作都可以使用CCMoveBy來實現CCNode的移動,實現CCSprite動畫可以使用CCAnimation。當動作執行時,一個異步的定時機製保留在了後台中。剛開始程序員通常會過度依賴這個功能。當執行多動作時使用這個技術可以提高效率。在下麵的例子中我們將使用一個整合計時器,它將允許我們
控製光照在屏幕上持續的時間。
//Randomly make lightning strike
if(arc4random()%70 == 7){
if(lightningRemoveCount [self addChild:lightningBolt z:1 tag:TAG_LIGHTNING_BOLT];
lightningRemoveCount = arc4random()%5+5;
}
}
//Count down
lightningRemoveCount -= 1;
//Clean up any old lightning bolts
if(lightningRemoveCount == 0){
[self removeChildByTag:TAG_LIGHTNING_BOLT cleanup:NO];
}
就前麵這段代碼來說,通常使用同步計時器較異步計時器更勝一籌,記住這個,特別是當你的遊戲規模越來越大時。
總結:
在本文中我們簡單介紹了sprites的基本使用方法。
最後更新:2017-04-03 12:56:05