55、基于Sprite Kit的游戏开发全流程指南

基于Sprite Kit的游戏开发全流程指南

1. 碰撞检测与处理基础

在游戏开发中,碰撞检测与处理是非常重要的环节。其核心思想是判断两个碰撞对象是否属于同一类别。若属于同一类别,它们可视为“友方”;若属于不同类别,则需确定谁是攻击者。在类别定义中,存在“攻击优先级”顺序,例如玩家节点可被敌人节点攻击,而敌人节点又可被玩家导弹节点攻击,我们可通过简单的大小比较来确定攻击者。

为了实现代码的简洁性和模块化,我们不希望场景直接决定每个对象被攻击或碰撞后的反应,而是将这些细节封装到受影响的节点类中。为此,我们可以利用多态性,让每个节点类以自己的方式处理碰撞。具体做法是为 SKNode 添加类别方法,默认实现为空,子类可根据需要进行重写。

为SKNode添加类别

以下是为 SKNode 添加类别的具体步骤:
1. 在Xcode的项目导航器中,右键点击 TextShooter 文件夹,从弹出菜单中选择 New File…
2. 在助手的 iOS/Source 部分,选择 Objective-C File ,然后点击 Next
3. 将文件名设置为 Extra ,选择 Category 作为文件类型,并选择 SKNode 作为要添加类别的类。
4. 再次点击 Next 并创建文件。
5. 选择类别头文件 SKNode+Extra.h ,添加以下方法声明:

#import <SpriteKit/SpriteKit.h>

@interface SKNode (Extra)

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact;
- (void)friendlyBumpFrom:(SKNode *)node;

@end
  1. 切换到对应的 .m 文件,输入以下空定义:
#import "SKNode+Extra.h"

@implementation SKNode (Extra)

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    // default implementation does nothing
}

- (void)friendlyBumpFrom:(SKNode *)node {
    // default implementation does nothing
}

@end
2. 完善GameScene中的碰撞处理

回到 GameScene.m 文件,完成碰撞处理部分。首先在文件顶部添加新的头文件:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"
#import "SKNode+Extra.h"

然后在 didBeginContact: 方法中添加实际处理逻辑:

- (void)didBeginContact:(SKPhysicsContact *)contact {
    if (contact.bodyA.categoryBitMask == contact.bodyB.categoryBitMask) {
        // Both bodies are in the same category
        SKNode *nodeA = contact.bodyA.node;
        SKNode *nodeB = contact.bodyB.node;

        // What do we do with these nodes?
        [nodeA friendlyBumpFrom:nodeB];
        [nodeB friendlyBumpFrom:nodeA];
    } else {
        SKNode *attacker = nil;
        SKNode *attackee = nil;

        if (contact.bodyA.categoryBitMask > contact.bodyB.categoryBitMask) {
            // Body A is attacking Body B
            attacker = contact.bodyA.node;
            attackee = contact.bodyB.node;
        } else {
            // Body B is attacking Body A
            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?
        [attackee receiveAttacker:attacker contact:contact];
        [self.playerBullets removeChildrenInArray:@[attacker]];
        [self.enemies removeChildrenInArray:@[attacker]];
    }
}

在上述代码中,如果碰撞属于“友方碰撞”,我们会通知双方节点;否则,确定攻击者和被攻击者后,通知被攻击者并从相应节点中移除攻击者。

3. 为敌人节点添加自定义碰撞行为

现在可以通过重写为 SKNode 添加的类别方法,为节点实现特定行为。打开 EnemyNode.m 文件,在文件顶部添加 Geometry.h 的导入:

#import "PhysicsCategories.h"
#import "Geometry.h"

@implementation EnemyNode

然后添加以下两个方法:

- (void)friendlyBumpFrom:(SKNode *)node {
    self.physicsBody.affectedByGravity = YES;
}

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    self.physicsBody.affectedByGravity = YES;
    CGVector force = VectorMultiply(attacker.physicsBody.velocity,
                                       contact.collisionImpulse);
    CGPoint myContact = [self.scene convertPoint:contact.contactPoint
                                          toNode:self];
    [self.physicsBody applyForce:force
                         atPoint:myContact];
}

friendlyBumpFrom: 方法会在敌人被友方碰撞时开启重力,使其开始下落; receiveAttacker:contact: 方法会在敌人被子弹击中时,不仅开启重力,还会根据碰撞数据施加额外的力。

4. 显示准确的玩家生命值

在游戏运行过程中,我们发现玩家生命值显示存在问题,始终显示为5。这是因为生命值显示在关卡创建时设置,但之后未更新。我们可以通过在 GameScene.m 中实现 setPlayerLives: 方法来解决这个问题:

- (void)setPlayerLives:(NSUInteger)playerLives  {
    _playerLives = playerLives;
    SKLabelNode *lives = (id)[self childNodeWithName:@"LivesLabel"];
    lives.text = [NSString stringWithFormat:@"Lives: %lu",
                  (unsigned long)_playerLives];
}

运行游戏后,玩家生命值会随着敌人的攻击而准确更新。但当生命值降为0后,游戏并未结束,而是出现了异常的大量生命值,这是因为我们尚未编写检测游戏结束的代码。

5. 使用粒子系统增强游戏视觉效果

Sprite Kit提供了粒子系统,可用于创建烟雾、火焰、爆炸等视觉效果。目前游戏中,子弹击中敌人或敌人击中玩家时,攻击对象只是简单消失,我们可以创建粒子系统来改善这种情况。

创建粒子系统

以下是创建粒子系统的步骤:
1. 按下 ⌘N 打开新文件助手。
2. 选择左侧的 iOS/Resource 部分,然后在右侧选择 SpriteKit Particle File
3. 点击 Next ,在接下来的屏幕中选择 Spark 粒子模板。
4. 再次点击 Next ,将文件命名为 MissileExplosion.sks

创建完成后,Xcode会生成粒子文件并添加 spark.png 资源。我们需要对粒子效果进行重新配置,使其更符合游戏需求。具体操作如下:
1. 按下 Opt-Cmd-7 打开 SKNode Inspector
2. 点击底部 Color Ramp 中的小颜色框,将颜色设置为黑色。
3. 将背景颜色改为白色,混合模式改为 Alpha
4. 逐个更改其他数值参数,直到达到目标效果。

按照相同的步骤,创建另一个名为 EnemyExplosion.sks 的粒子系统,并设置其参数。

将粒子系统应用到游戏中

EnemyNode.m receiveAttacker:contact: 方法底部添加以下代码:

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    self.physicsBody.affectedByGravity = YES;
    CGVector force = VectorMultiply(attacker.physicsBody.velocity,
                                       contact.collisionImpulse);
    CGPoint myContact = [self.scene convertPoint:contact.contactPoint
                                          toNode:self];
    [self.physicsBody applyForce:force
                         atPoint:myContact];

    NSString *path = [[NSBundle mainBundle] pathForResource:@"MissileExplosion"
                                                     ofType:@"sks"];
    SKEmitterNode *explosion = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    explosion.numParticlesToEmit = 20;
    explosion.position = contact.contactPoint;
    [self.scene addChild:explosion];
}

PlayerNode.m 中添加以下方法:

- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"EnemyExplosion"
                                                     ofType:@"sks"];
    SKEmitterNode *explosion =
           [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    explosion.numParticlesToEmit = 50;
    explosion.position = contact.contactPoint;
    [self.scene addChild:explosion];
}

运行游戏后,子弹击中敌人会出现小爆炸效果,敌人击中玩家会出现红色飞溅效果,大大增强了游戏的视觉体验。

以下是整个流程的mermaid流程图:

graph LR
    A[碰撞检测] --> B{是否同一类别}
    B -- 是 --> C[友方碰撞处理]
    B -- 否 --> D[确定攻击者和被攻击者]
    D --> E[处理攻击行为]
    E --> F[更新玩家生命值]
    F --> G{生命值是否为0}
    G -- 是 --> H[游戏结束处理]
    G -- 否 --> I[继续游戏]
    I --> J[粒子效果处理]
6. 实现游戏结束与开始场景

为了处理游戏结束的情况,我们需要创建一个新的场景类 GameOverScene 。具体步骤如下:
1. 创建一个新的 iOS/Cocoa Touch 类,使用 SKScene 作为父类,命名为 GameOverScene
2. 在 GameOverScene.m @implementation 中添加以下代码:

- (instancetype)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor purpleColor];
        SKLabelNode *text = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        text.text = @"Game Over";
        text.fontColor = [SKColor whiteColor];
        text.fontSize = 50;
        text.position = CGPointMake(self.frame.size.width * 0.5,
                                    self.frame.size.height * 0.5);
        [self addChild:text];
    }
    return self;
}

GameScene.m 中,我们需要导入 GameOverScene.h 头文件,并添加以下方法:

#import "GameScene.h"
#import "PlayerNode.h"
#import "EnemyNode.h"
#import "BulletNode.h"
#import "SKNode+Extra.h"
#import "GameOverScene.h"

- (void)triggerGameOver {
    self.finished = YES;

    NSString *path = [[NSBundle mainBundle] pathForResource:@"EnemyExplosion"
                                                     ofType:@"sks"];
    SKEmitterNode *explosion =
               [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    explosion.numParticlesToEmit = 200;
    explosion.position = _playerNode.position;
    [self addChild:explosion];
    [_playerNode removeFromParent];

    SKTransition *transition =
             [SKTransition doorsOpenVerticalWithDuration:1.0];
    SKScene *gameOver = [[GameOverScene alloc] initWithSize:self.frame.size];
    [self.view presentScene:gameOver transition:transition];
}

- (BOOL)checkForGameOver {
    if (self.playerLives == 0) {
        [self triggerGameOver];
        return YES;
    }
    return NO;
}

- (void)update:(CFTimeInterval)currentTime {
    if (self.finished) return;

    [self updateBullets];
    [self updateEnemies];
    if (![self checkForGameOver]) {
        [self checkForNextLevel];
    }
}

triggerGameOver 方法用于触发游戏结束,包括显示额外的爆炸效果和切换到游戏结束场景; checkForGameOver 方法用于检查游戏是否结束; update 方法在每次更新时检查游戏状态,确保游戏结束时不会出现异常的场景切换。

7. 创建开始场景

为了让游戏有一个开始界面,避免玩家在启动时直接进入游戏,我们创建一个 StartScene 。具体步骤如下:
1. 创建一个新的 iOS/Cocoa Touch 类,使用 SKScene 作为父类,命名为 StartScene
2. 在 StartScene.m 中添加以下代码:

#import "StartScene.h"
#import "GameScene.h"

@implementation StartScene

- (instancetype)initWithSize:(CGSize)size {
    if (self = [super initWithSize:size]) {
        self.backgroundColor = [SKColor greenColor];

        SKLabelNode *topLabel = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        topLabel.text = @"TextShooter";
        topLabel.fontColor = [SKColor blackColor];
        topLabel.fontSize = 48;
        topLabel.position = CGPointMake(self.frame.size.width * 0.5,
                                    self.frame.size.height * 0.7);
        [self addChild:topLabel];

        SKLabelNode *bottomLabel = [SKLabelNode labelNodeWithFontNamed:
                                    @"Courier"];
        bottomLabel.text = @"Touch anywhere to start";
        bottomLabel.fontColor = [SKColor blackColor];
        bottomLabel.fontSize = 20;
        bottomLabel.position = CGPointMake(self.frame.size.width * 0.5,
                                        self.frame.size.height * 0.3);
        [self addChild:bottomLabel];
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    SKTransition *transition = [SKTransition doorwayWithDuration:1.0];
    SKScene *game = [[GameScene alloc] initWithSize:self.frame.size];
    [self.view presentScene:game transition:transition];
}

@end

GameOverScene.m 中,我们需要导入 StartScene.h 头文件,并添加以下代码,使游戏结束场景在一段时间后返回开始场景:

#import "GameOverScene.h"
#import "StartScene.h"

- (void)didMoveToView:(SKView *)view {
    dispatch_after(
            dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)),
            dispatch_get_main_queue(), ^{
        SKTransition *transition = [SKTransition flipVerticalWithDuration:1.0];
        SKScene *start = [[StartScene alloc] initWithSize:self.frame.size];
        [self.view presentScene:start transition:transition];
    });
}

最后,在 GameViewController.m 中,我们需要导入 StartScene.h 头文件,并修改 viewDidLoad 方法,使应用启动时显示开始场景:

#import "GameViewController.h"
#import "GameScene.h"
#import "StartScene.h"

// 修改前
// GameScene *scene = [GameScene sceneWithSize:self.view.frame.size levelNumber:1];
// 修改后
SKScene * scene = [StartScene sceneWithSize:skView.bounds.size];

现在,启动应用后会显示开始场景,玩家触摸屏幕即可开始游戏,游戏结束后等待几秒会返回开始场景,形成一个完整的游戏循环。

以下是场景切换的mermaid流程图:

graph LR
    A[开始场景] --> B[游戏场景]
    B --> C{游戏是否结束}
    C -- 是 --> D[游戏结束场景]
    D --> E[等待3秒]
    E --> A[开始场景]
    C -- 否 --> B[游戏场景]

通过以上步骤,我们完成了一个基于Sprite Kit的完整游戏开发流程,包括碰撞检测、粒子效果、游戏结束和开始场景的实现,大大提升了游戏的趣味性和用户体验。

基于Sprite Kit的游戏开发全流程指南

8. 总结与回顾

在本次游戏开发过程中,我们逐步实现了多个关键功能,让游戏从简单的基础框架变得更加完善和有趣。下面我们对整个开发流程进行一个总结回顾。

功能模块 主要实现内容
碰撞检测与处理 判断碰撞对象类别,区分友方碰撞和攻击行为,通过为 SKNode 添加类别方法实现多态处理
玩家生命值显示 修复生命值显示问题,确保生命值随攻击准确更新,并添加游戏结束检测逻辑
粒子系统应用 创建并配置粒子系统,增强子弹击中敌人和敌人击中玩家时的视觉效果
游戏场景管理 实现游戏结束场景 GameOverScene 和开始场景 StartScene ,并处理场景之间的切换

通过这些功能的实现,我们不仅让游戏在玩法上更加完整,还在视觉效果上有了很大的提升,为玩家带来了更好的游戏体验。

9. 可能的优化方向

虽然我们已经完成了一个基本的游戏,但仍然有很多可以优化的地方,以下是一些可能的优化方向:

  • 性能优化 :在游戏中,粒子系统和物理模拟可能会消耗较多的性能。我们可以通过减少粒子数量、优化物理模拟参数等方式来提高游戏的性能。例如,在粒子系统中,可以适当减少 numParticlesToEmit 的值,或者优化粒子的生命周期和发射频率。
  • 游戏玩法扩展 :可以添加更多的游戏元素和玩法,如道具系统、关卡设计、排行榜等。例如,添加道具系统可以让玩家在游戏中获得各种增益效果,增加游戏的趣味性和策略性。
  • 用户界面优化 :改善游戏的用户界面,使其更加美观和易用。可以设计更加精美的开始场景和游戏结束场景,添加更多的交互元素,如按钮、动画等。
10. 代码复用与模块化

在开发过程中,我们采用了一些模块化的设计思想,如为 SKNode 添加类别方法,将碰撞处理逻辑封装到节点类中。这种模块化的设计不仅提高了代码的可读性和可维护性,还方便了代码的复用。

例如,我们可以将粒子系统的创建和配置代码封装成一个独立的工具类,在需要使用粒子效果的地方直接调用该工具类的方法,这样可以避免代码的重复编写。以下是一个简单的粒子系统工具类示例:

#import <SpriteKit/SpriteKit.h>

@interface ParticleSystemTool : NSObject

+ (SKEmitterNode *)createMissileExplosionParticle;
+ (SKEmitterNode *)createEnemyExplosionParticle;

@end

@implementation ParticleSystemTool

+ (SKEmitterNode *)createMissileExplosionParticle {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MissileExplosion" ofType:@"sks"];
    SKEmitterNode *explosion = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    explosion.numParticlesToEmit = 20;
    return explosion;
}

+ (SKEmitterNode *)createEnemyExplosionParticle {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"EnemyExplosion" ofType:@"sks"];
    SKEmitterNode *explosion = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
    explosion.numParticlesToEmit = 50;
    return explosion;
}

@end

在需要使用粒子效果的地方,可以直接调用这些方法:

// 在EnemyNode.m的receiveAttacker:contact:方法中
- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    // 其他代码...
    SKEmitterNode *explosion = [ParticleSystemTool createMissileExplosionParticle];
    explosion.position = contact.contactPoint;
    [self.scene addChild:explosion];
}

// 在PlayerNode.m的receiveAttacker:contact:方法中
- (void)receiveAttacker:(SKNode *)attacker contact:(SKPhysicsContact *)contact {
    SKEmitterNode *explosion = [ParticleSystemTool createEnemyExplosionParticle];
    explosion.position = contact.contactPoint;
    [self.scene addChild:explosion];
}

通过这种方式,我们可以提高代码的复用性,减少代码的冗余,同时也方便了代码的维护和扩展。

11. 未来展望

随着游戏开发技术的不断发展,我们可以在现有基础上进一步拓展游戏的功能和玩法。例如,结合虚拟现实(VR)或增强现实(AR)技术,为玩家带来更加沉浸式的游戏体验;利用网络技术,实现多人在线对战功能,增加游戏的社交性和互动性。

此外,我们还可以不断优化游戏的性能和用户体验,根据玩家的反馈和市场需求,持续更新和改进游戏。相信通过不断的努力和创新,我们可以开发出更加优秀的游戏作品。

12. 开发经验分享

在本次游戏开发过程中,我们积累了一些宝贵的经验,希望能对其他开发者有所帮助。

  • 注重代码结构和模块化 :良好的代码结构和模块化设计可以提高代码的可读性、可维护性和复用性。在开发过程中,要尽量将不同的功能模块分离,避免代码的耦合度过高。
  • 多进行测试和调试 :在开发过程中,要及时进行测试和调试,发现并解决问题。可以使用模拟器和真机进行测试,确保游戏在不同设备上都能正常运行。
  • 参考优秀的开源项目 :参考优秀的开源项目可以学习到其他开发者的经验和技巧,同时也可以借鉴他们的代码结构和设计思路。

总之,游戏开发是一个充满挑战和乐趣的过程,通过不断学习和实践,我们可以不断提高自己的开发水平,开发出更加优秀的游戏作品。

以下是整个游戏开发流程的总结mermaid流程图:

graph LR
    A[初始化项目] --> B[碰撞检测与处理]
    B --> C[玩家生命值管理]
    C --> D[粒子系统应用]
    D --> E[游戏场景管理]
    E --> F[性能优化与扩展]
    F --> G[代码复用与模块化]
    G --> H[持续更新与改进]

通过以上的开发流程和优化方向,我们可以不断完善游戏,为玩家带来更好的游戏体验。希望本次的开发经验能对大家有所帮助,让我们一起在游戏开发的道路上不断前进!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值