基于Sprite Kit的游戏开发全流程指南
1. 游戏初步体验与敌人创建
运行游戏应用程序,你会看到飞船在来回移动时会愉快地摆动,看起来就像在行走一样。不过,这个游戏需要一些敌人供玩家射击。
首先,使用Xcode创建一个新的Cocoa Touch类,名为
EnemyNode
,并以
SKNode
作为父类。目前,我们不会给敌人类添加任何实际行为,但会赋予它外观。我们将使用与创建玩家相同的技术,用文本构建敌人的身体。以下是
EnemyNode.m
中的代码:
- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Enemy %p", self];
[self initNodeGraph];
}
return self;
}
- (void)initNodeGraph {
SKLabelNode *topRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
topRow.fontColor = [SKColor brownColor];
topRow.fontSize = 20;
topRow.text = @"x x";
topRow.position = CGPointMake(0, 15);
[self addChild:topRow];
SKLabelNode *middleRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
middleRow.fontColor = [SKColor brownColor];
middleRow.fontSize = 20;
middleRow.text = @"x";
[self addChild:middleRow];
SKLabelNode *bottomRow = [SKLabelNode
labelNodeWithFontNamed:@"Courier-Bold"];
bottomRow.fontColor = [SKColor brownColor];
bottomRow.fontSize = 20;
bottomRow.text = @"x x";
bottomRow.position = CGPointMake(0, -15);
[self addChild:bottomRow];
}
这里只是通过改变每个文本“行”的y值来添加多行文本。
2. 将敌人添加到场景中
接下来,我们要让敌人出现在场景中,需要对
GameScene.m
进行一些修改:
1. 在文件顶部添加以下代码:
#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
@interface GameScene ()
@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;
@end
这里导入了新敌人类的头文件,并添加了一个新属性来保存所有将添加到关卡中的敌人。使用
SKNode
来保存敌人是因为它可以容纳任意数量的子节点,便于管理。
2. 创建
spawnEnemies
方法:
- (void)spawnEnemies {
NSUInteger count = log(self.levelNumber) + self.levelNumber;
for (NSUInteger i = 0; i < count; i++) {
EnemyNode *enemy = [EnemyNode node];
CGSize size = self.frame.size;
CGFloat x = arc4random_uniform(size.width * 0.8)
+ (size.width * 0.1);
CGFloat y = arc4random_uniform(size.height * 0.5)
+ (size.height * 0.5);
enemy.position = CGPointMake(x, y);
[self.enemies addChild:enemy];
}
}
-
在
initWithSize:levelNumber:方法的末尾添加以下代码:
[self addChild:_playerNode];
_enemies = [SKNode node];
[self addChild:_enemies];
[self spawnEnemies];
运行应用程序,你会看到可怕的敌人随机出现在屏幕的上半部分。
3. 开始射击功能的实现
现在是时候让玩家能够攻击敌人了。我们希望玩家可以点击屏幕上半部分的任意位置向敌人射击子弹。这里将使用Sprite Kit中包含的物理引擎来移动玩家的子弹,并检测子弹与敌人的碰撞。
3.1 物理引擎的概念
物理引擎是一个软件组件,它跟踪一个世界中的多个物理对象(通常称为物体)以及作用在它们上的力,确保一切以逼真的方式移动。它可以考虑重力、处理物体之间的碰撞,甚至模拟摩擦和弹性等物理特性。需要注意的是,物理引擎通常与图形引擎是分开的。
3.2 定义物理类别
Sprite Kit物理引擎允许我们将对象分配到几个不同的物理类别中。在这个游戏中,我们将创建三个类别:敌人、玩家和玩家导弹。以下是创建这些类别的代码:
#ifndef TextShooter_PhysicsCategories_h
#define TextShooter_PhysicsCategories_h
typedef NS_OPTIONS(uint32_t, PhysicsCategory) {
PlayerCategory = 1 << 1,
EnemyCategory = 1 << 2,
PlayerMissileCategory = 1 << 3
};
#endif
这些类别使用位掩码,每个类别必须是2的幂,这样可以简化物理引擎的API。
3.3 创建BulletNode类
创建一个新的Cocoa Touch类
BulletNode
,以
SKNode
为父类。在头文件中声明两个公共方法:
#import <SpriteKit/SpriteKit.h>
@interface BulletNode : SKNode
+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination;
- (void)applyRecurringForce;
@end
在
BulletNode.m
中实现这些方法:
#import "BulletNode.h"
#import "PhysicsCategories.h"
#import "Geometry.h"
@interface BulletNode ()
@property (assign, nonatomic) CGVector thrust;
@end
@implementation BulletNode
- (instancetype)init {
if (self = [super init]) {
SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
dot.fontColor = [SKColor blackColor];
dot.fontSize = 40;
dot.text = @".";
[self addChild:dot];
SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
body.dynamic = YES;
body.categoryBitMask = PlayerMissileCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = EnemyCategory;
body.mass = 0.01;
self.physicsBody = body;
self.name = [NSString stringWithFormat:@"Bullet %p", self];
}
return self;
}
+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination {
BulletNode *bullet = [[self alloc] init];
bullet.position = start;
CGVector movement = VectorBetweenPoints(start, destination);
CGFloat magnitude = VectorLength(movement);
if (magnitude == 0.0f) return nil;
CGVector scaledMovement = VectorMultiply(movement, 1 / magnitude);
CGFloat thrustMagnitude = 100.0;
bullet.thrust = VectorMultiply(scaledMovement, thrustMagnitude);
return bullet;
}
- (void)applyRecurringForce {
[self.physicsBody applyForce:self.thrust];
}
3.4 将子弹添加到场景中
在
GameScene.m
中添加子弹到场景:
1. 导入新类的头文件并添加一个属性来保存所有子弹:
#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"
@interface GameScene ()
@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) SKNode *enemies;
@property (strong, nonatomic) SKNode *playerBullets;
@end
-
在
initWithSize:levelNumber:方法中设置playerBullets节点:
[self spawnEnemies];
_playerBullets = [SKNode node];
[self addChild:_playerBullets];
-
修改
touchesBegan:withEvent:方法,使屏幕上半部分的点击可以发射子弹:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
if (location.y < CGRectGetHeight(self.frame) * 0.2 ) {
CGPoint target = CGPointMake(location.x,
self.playerNode.position.y);
[self.playerNode moveToward:target];
} else {
BulletNode *bullet = [BulletNode
bulletFrom:self.playerNode.position
toward:location];
[self.playerBullets addChild:bullet];
}
}
}
-
在
update:方法中调用updateBullets方法来更新子弹状态:
- (void)update:(CFTimeInterval)currentTime {
[self updateBullets];
}
- (void)updateBullets {
NSMutableArray *bulletsToRemove = [NSMutableArray array];
for (BulletNode *bullet in self.playerBullets.children) {
if (!CGRectContainsPoint(self.frame, bullet.position)) {
[bulletsToRemove addObject:bullet];
continue;
}
[bullet applyRecurringForce];
}
[self.playerBullets removeChildrenInArray:bulletsToRemove];
}
运行应用程序,你会发现除了移动玩家的飞船,还可以通过点击屏幕发射向上的导弹。
4. 使用物理引擎攻击敌人
目前游戏还缺少一些重要的游戏元素,比如敌人不会攻击我们,我们也不能通过射击消灭敌人。现在我们要解决后一个问题,让射击敌人能将其从屏幕上击落。
4.1 为节点添加物理体
在
EnemyNode.m
中添加物理体:
#import "PhysicsCategories.h"
- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Enemy %p", self];
[self initNodeGraph];
[self initPhysicsBody];
}
return self;
}
- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(40, 40)];
body.affectedByGravity = NO;
body.categoryBitMask = EnemyCategory;
body.contactTestBitMask = PlayerCategory|EnemyCategory;
body.mass = 0.2;
body.angularDamping = 0.0f;
body.linearDamping = 0.0f;
self.physicsBody = body;
}
在
PlayerNode.m
中添加物理体:
#import "PhysicsCategories.h"
- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Player %p", self];
[self initNodeGraph];
[self initPhysicsBody];
}
return self;
}
- (void)initPhysicsBody {
SKPhysicsBody *body = [SKPhysicsBody bodyWithRectangleOfSize:
CGSizeMake(20, 20)];
body.affectedByGravity = NO;
body.categoryBitMask = PlayerCategory;
body.contactTestBitMask = EnemyCategory;
body.collisionBitMask = 0;
self.physicsBody = body;
}
运行应用程序,你会发现子弹现在可以将敌人击飞到太空中,但当唯一的敌人被击走后,游戏就卡住了,所以需要添加关卡管理。
5. 关卡管理
5.1 跟踪敌人状态
添加
updateEnemies
方法来移除离开屏幕的敌人:
- (void)updateEnemies {
NSMutableArray *enemiesToRemove = [NSMutableArray array];
for (SKNode *node in self.enemies.children) {
if (!CGRectContainsPoint(self.frame, node.position)) {
[enemiesToRemove addObject:node];
continue;
}
}
if ([enemiesToRemove count] > 0) {
[self.enemies removeChildrenInArray:enemiesToRemove];
}
}
修改
update:
方法:
- (void)update:(CFTimeInterval)currentTime {
if (self.finished) return;
[self updateBullets];
[self updateEnemies];
[self checkForNextLevel];
}
添加
checkForNextLevel
方法:
- (void)checkForNextLevel {
if ([self.enemies.children count] == 0) {
[self goToNextLevel];
}
}
5.2 过渡到下一关
实现
goToNextLevel
方法:
- (void)goToNextLevel {
self.finished = YES;
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
label.text = @"Level Complete!";
label.fontColor = [SKColor blueColor];
label.fontSize = 32;
label.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.5);
[self addChild:label];
GameScene *nextLevel = [[GameScene alloc]
initWithSize:self.frame.size
levelNumber:self.levelNumber + 1];
nextLevel.playerLives = self.playerLives;
[self.view presentScene:nextLevel
transition:[SKTransition flipHorizontalWithDuration:1.0]];
}
运行应用程序并完成一个关卡,你会看到关卡过渡效果。
6. 自定义碰撞处理
目前游戏虽然可以玩,但缺乏挑战性。我们要让敌人在被碰撞时掉落,并且被掉落的敌人击中会使玩家失去一条生命。同时,解决子弹击中敌人后奇怪的轨迹问题。
在
GameScene.m
中实现碰撞处理:
1. 在类扩展声明中添加代理协议声明:
@interface GameScene () <SKPhysicsContactDelegate>
-
在
initWithSize:levelNumber:方法中配置物理世界:
self.physicsWorld.gravity = CGVectorMake(0, -1);
self.physicsWorld.contactDelegate = self;
-
实现
didBeginContact:方法:
- (void)didBeginContact:(SKPhysicsContact *)contact {
if (contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask) {
SKNode *nodeA = contact.bodyA.node;
SKNode *nodeB = contact.bodyB.node;
// What do we do with these nodes?
} else {
SKNode *attacker = nil;
SKNode *attackee = nil;
if (contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask) {
attacker = contact.bodyA.node;
attackee = contact.bodyB.node;
} else {
attacker = contact.bodyB.node;
attackee = contact.bodyA.node;
}
if ([attackee isKindOfClass:[PlayerNode class]]) {
self.playerLives--;
}
// What do we do with the attacker and the attackee?
}
}
目前这个方法的具体功能还未完善,仅在敌人击中玩家时减少玩家的生命数。
总结
通过以上步骤,我们逐步实现了一个基于Sprite Kit的游戏,包括敌人的创建、射击功能、关卡管理和碰撞处理等。整个开发过程涉及到多个类的创建和修改,以及物理引擎的使用。以下是整个开发流程的mermaid流程图:
graph TD;
A[创建EnemyNode类] --> B[将敌人添加到场景中];
B --> C[实现射击功能];
C --> D[为节点添加物理体];
D --> E[实现关卡管理];
E --> F[自定义碰撞处理];
同时,为了更清晰地展示各个类的主要方法和功能,我们可以用表格呈现:
| 类名 | 主要方法 | 功能 |
| ---- | ---- | ---- |
| EnemyNode | init, initNodeGraph, initPhysicsBody | 创建敌人节点,设置外观和物理体 |
| GameScene | spawnEnemies, touchesBegan, update, updateBullets, updateEnemies, checkForNextLevel, goToNextLevel, didBeginContact | 管理场景,生成敌人,处理触摸事件,更新子弹和敌人状态,关卡管理和碰撞处理 |
| BulletNode | bulletFrom, applyRecurringForce, init | 创建子弹节点,设置推力和物理体 |
通过这些步骤和代码,你可以开发出一个具有基本功能的游戏,并根据需求进一步扩展和优化。
基于Sprite Kit的游戏开发全流程指南(续)
7. 优化敌人行为
目前敌人只是简单地出现在屏幕上,被击中后飞走。为了增加游戏的趣味性,我们可以让敌人具有更复杂的行为,比如移动和攻击。
7.1 让敌人移动
在
EnemyNode.m
中添加移动逻辑。首先,在类扩展中添加一个属性来保存敌人的移动方向:
@interface EnemyNode ()
@property (assign, nonatomic) CGVector movementDirection;
@end
然后在
init
方法中初始化移动方向:
- (instancetype)init {
if (self = [super init]) {
self.name = [NSString stringWithFormat:@"Enemy %p", self];
[self initNodeGraph];
[self initPhysicsBody];
// 随机初始化移动方向
CGFloat randomX = (arc4random_uniform(2) == 0)? -1 : 1;
CGFloat randomY = (arc4random_uniform(2) == 0)? -1 : 1;
self.movementDirection = CGVectorMake(randomX, randomY);
}
return self;
}
接着添加一个方法来更新敌人的位置:
- (void)updatePosition {
CGPoint currentPosition = self.position;
CGFloat speed = 2; // 敌人移动速度
currentPosition.x += self.movementDirection.dx * speed;
currentPosition.y += self.movementDirection.dy * speed;
// 边界检测
CGSize sceneSize = self.scene.size;
if (currentPosition.x < 0 || currentPosition.x > sceneSize.width) {
self.movementDirection.dx = -self.movementDirection.dx;
}
if (currentPosition.y < 0 || currentPosition.y > sceneSize.height) {
self.movementDirection.dy = -self.movementDirection.dy;
}
self.position = currentPosition;
}
最后,在
GameScene.m
的
update:
方法中调用这个方法来更新所有敌人的位置:
- (void)update:(CFTimeInterval)currentTime {
if (self.finished) return;
[self updateBullets];
[self updateEnemies];
[self checkForNextLevel];
// 更新敌人位置
for (EnemyNode *enemy in self.enemies.children) {
[enemy updatePosition];
}
}
7.2 让敌人攻击
为了让敌人能够攻击玩家,我们可以让敌人发射子弹。首先,创建一个新的类
EnemyBulletNode
,类似于
BulletNode
:
// EnemyBulletNode.h
#import <SpriteKit/SpriteKit.h>
@interface EnemyBulletNode : SKNode
+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination;
- (void)applyRecurringForce;
@end
// EnemyBulletNode.m
#import "EnemyBulletNode.h"
#import "PhysicsCategories.h"
#import "Geometry.h"
@interface EnemyBulletNode ()
@property (assign, nonatomic) CGVector thrust;
@end
@implementation EnemyBulletNode
- (instancetype)init {
if (self = [super init]) {
SKLabelNode *dot = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
dot.fontColor = [SKColor redColor];
dot.fontSize = 40;
dot.text = @"*";
[self addChild:dot];
SKPhysicsBody *body = [SKPhysicsBody bodyWithCircleOfRadius:1];
body.dynamic = YES;
body.categoryBitMask = EnemyMissileCategory; // 新增敌人子弹类别
body.contactTestBitMask = PlayerCategory;
body.collisionBitMask = PlayerCategory;
body.mass = 0.01;
self.physicsBody = body;
self.name = [NSString stringWithFormat:@"EnemyBullet %p", self];
}
return self;
}
+ (instancetype)bulletFrom:(CGPoint)start toward:(CGPoint)destination {
EnemyBulletNode *bullet = [[self alloc] init];
bullet.position = start;
CGVector movement = VectorBetweenPoints(start, destination);
CGFloat magnitude = VectorLength(movement);
if (magnitude == 0.0f) return nil;
CGVector scaledMovement = VectorMultiply(movement, 1 / magnitude);
CGFloat thrustMagnitude = 100.0;
bullet.thrust = VectorMultiply(scaledMovement, thrustMagnitude);
return bullet;
}
- (void)applyRecurringForce {
[self.physicsBody applyForce:self.thrust];
}
@end
在
EnemyNode.m
中添加发射子弹的方法:
- (void)shootAtPlayer:(PlayerNode *)player {
EnemyBulletNode *bullet = [EnemyBulletNode bulletFrom:self.position toward:player.position];
[self.scene addChild:bullet];
}
在
GameScene.m
中,让敌人定期发射子弹。可以在
update:
方法中添加以下逻辑:
- (void)update:(CFTimeInterval)currentTime {
if (self.finished) return;
[self updateBullets];
[self updateEnemies];
[self checkForNextLevel];
// 更新敌人位置
for (EnemyNode *enemy in self.enemies.children) {
[enemy updatePosition];
// 每隔一段时间让敌人发射子弹
static CFTimeInterval lastShotTime = 0;
if (currentTime - lastShotTime > 2.0) { // 每2秒发射一次
[enemy shootAtPlayer:self.playerNode];
lastShotTime = currentTime;
}
}
}
8. 完善碰撞处理
之前的碰撞处理逻辑只是简单地减少玩家的生命数,现在要进一步完善,比如移除被击中的敌人和子弹。
在
GameScene.m
的
didBeginContact:
方法中添加以下代码:
- (void)didBeginContact:(SKPhysicsContact *)contact {
SKPhysicsBody *firstBody;
SKPhysicsBody *secondBody;
if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
firstBody = contact.bodyA;
secondBody = contact.bodyB;
} else {
firstBody = contact.bodyB;
secondBody = contact.bodyA;
}
if ((firstBody.categoryBitMask & PlayerMissileCategory) != 0 &&
(secondBody.categoryBitMask & EnemyCategory) != 0) {
// 玩家子弹击中敌人
[firstBody.node removeFromParent];
[secondBody.node removeFromParent];
} else if ((firstBody.categoryBitMask & EnemyMissileCategory) != 0 &&
(secondBody.categoryBitMask & PlayerCategory) != 0) {
// 敌人子弹击中玩家
[firstBody.node removeFromParent];
self.playerLives--;
if (self.playerLives <= 0) {
// 玩家死亡,游戏结束
[self gameOver];
}
}
}
添加
gameOver
方法:
- (void)gameOver {
self.finished = YES;
SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
label.text = @"Game Over!";
label.fontColor = [SKColor redColor];
label.fontSize = 32;
label.position = CGPointMake(self.frame.size.width * 0.5,
self.frame.size.height * 0.5);
[self addChild:label];
// 可以在这里添加重新开始游戏的逻辑
}
9. 增加游戏界面元素
为了让游戏更加直观,我们可以增加一些界面元素,比如显示玩家的生命数和当前关卡。
在
GameScene.m
的
initWithSize:levelNumber:
方法中添加以下代码:
- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
if (self = [super initWithSize:size]) {
self.levelNumber = levelNumber;
self.playerLives = 3; // 初始化玩家生命数
// 创建玩家节点
_playerNode = [PlayerNode node];
_playerNode.position = CGPointMake(size.width * 0.5, size.height * 0.1);
[self addChild:_playerNode];
// 创建敌人节点容器
_enemies = [SKNode node];
[self addChild:_enemies];
// 创建玩家子弹节点容器
_playerBullets = [SKNode node];
[self addChild:_playerBullets];
// 显示玩家生命数
SKLabelNode *livesLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
livesLabel.text = [NSString stringWithFormat:@"Lives: %lu", (unsigned long)self.playerLives];
livesLabel.fontColor = [SKColor whiteColor];
livesLabel.fontSize = 20;
livesLabel.position = CGPointMake(20, size.height - 20);
[self addChild:livesLabel];
self.livesLabel = livesLabel; // 在类扩展中添加属性保存这个标签
// 显示当前关卡
SKLabelNode *levelLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
levelLabel.text = [NSString stringWithFormat:@"Level: %lu", (unsigned long)self.levelNumber];
levelLabel.fontColor = [SKColor whiteColor];
levelLabel.fontSize = 20;
levelLabel.position = CGPointMake(size.width - 80, size.height - 20);
[self addChild:levelLabel];
self.levelLabel = levelLabel; // 在类扩展中添加属性保存这个标签
[self spawnEnemies];
self.physicsWorld.gravity = CGVectorMake(0, -1);
self.physicsWorld.contactDelegate = self;
}
return self;
}
在
update:
方法中更新生命数和关卡标签:
- (void)update:(CFTimeInterval)currentTime {
if (self.finished) return;
[self updateBullets];
[self updateEnemies];
[self checkForNextLevel];
// 更新敌人位置
for (EnemyNode *enemy in self.enemies.children) {
[enemy updatePosition];
// 每隔一段时间让敌人发射子弹
static CFTimeInterval lastShotTime = 0;
if (currentTime - lastShotTime > 2.0) { // 每2秒发射一次
[enemy shootAtPlayer:self.playerNode];
lastShotTime = currentTime;
}
}
// 更新生命数标签
self.livesLabel.text = [NSString stringWithFormat:@"Lives: %lu", (unsigned long)self.playerLives];
// 更新关卡标签
self.levelLabel.text = [NSString stringWithFormat:@"Level: %lu", (unsigned long)self.levelNumber];
}
总结与展望
通过以上步骤,我们对游戏进行了多方面的优化,包括敌人行为、碰撞处理和界面元素。整个开发过程可以用以下mermaid流程图表示:
graph TD;
A[优化敌人行为] --> B[完善碰撞处理];
B --> C[增加游戏界面元素];
C --> D[游戏完成并可进一步扩展];
同时,为了更清晰地展示各个类的新增方法和功能,我们更新表格如下:
| 类名 | 新增主要方法 | 功能 |
| ---- | ---- | ---- |
| EnemyNode | updatePosition, shootAtPlayer | 让敌人移动和攻击玩家 |
| EnemyBulletNode | bulletFrom, applyRecurringForce, init | 创建敌人子弹节点,设置推力和物理体 |
| GameScene | didBeginContact(完善), gameOver, 界面元素相关代码 | 完善碰撞处理,处理游戏结束,增加界面元素 |
现在游戏已经具备了更丰富的功能和更好的用户体验。未来,你可以进一步扩展游戏,比如增加更多的关卡、不同类型的敌人和武器,或者添加音效和动画效果,让游戏更加精彩。通过不断地学习和实践,你可以开发出更加复杂和有趣的游戏。
超级会员免费看
12

被折叠的 条评论
为什么被折叠?



