53、Sprite Kit 开发入门:从场景定制到玩家交互

Sprite Kit 开发入门:从场景定制到玩家交互

在使用 Sprite Kit 进行游戏开发时,选择合适的缩放模式至关重要,它取决于应用程序的具体需求。若现有的缩放模式都不适用,还有另外两种选择:
1. 支持一组固定的屏幕尺寸,为每个尺寸创建单独的设计,并将其存储在各自的 .sks 文件中,在需要时从正确的文件加载场景。
2. 通过代码创建场景,使其与呈现它的 SKView 大小相同,并以编程方式填充节点。不过,这种方法仅适用于游戏元素的相对位置不依赖于精确值的情况。下面我们将以 TextShooter 应用程序为例,详细介绍这种方法的实现过程。

初始场景定制
  1. 清理代码 :打开 TextShooter 项目,选择 GameScene 类,删除 Xcode 模板生成的大部分代码。具体操作如下:
    • 删除整个 didMoveToView: 方法,该方法在场景显示在 SKView 中时调用,通常用于在场景可见之前进行最后的修改。
    • 移除 touchesBegan:withEvent: 方法的大部分内容,只保留 for 循环及其包含的第一行代码。此时, GameScene 类的代码如下:
@implementation GameScene

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */

    for (UITouch *touch in touches) {
        CGPoint location = [touch locationInNode:self];
    }
}

-(void)update:(CFTimeInterval)currentTime {
    /* Called before each frame is rendered */
}

@end
  1. 添加属性和方法声明 :由于我们不打算从 GameScene.sks 文件加载场景,因此需要一个方法来为我们创建带有初始内容的场景。同时,还需要添加当前游戏关卡编号、玩家生命数以及关卡是否完成的标志等属性。在 GameScene.h 中添加以下代码:
@interface GameScene : SKScene

@property (assign, nonatomic) NSUInteger levelNumber;
@property (assign, nonatomic) NSUInteger playerLives;
@property (assign, nonatomic) BOOL finished;

+ (instancetype)sceneWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber;
- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber;

@end
  1. 实现新方法 :切换到 GameScene.m 文件,实现刚刚声明的两个新方法。添加以下代码:
+ (instancetype)sceneWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    return [[self alloc] initWithSize:size levelNumber:levelNumber];
}

- (instancetype)initWithSize:(CGSize)size {
    return [self initWithSize:size levelNumber:1];
}

- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    if (self = [super initWithSize:size]) {
        _levelNumber = levelNumber;
        _playerLives = 5;

        self.backgroundColor = [SKColor whiteColor];

        SKLabelNode *lives = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        lives.fontSize = 16;
        lives.fontColor = [SKColor blackColor];
        lives.name = @"LivesLabel";
        lives.text = [NSString stringWithFormat:@"Lives: %lu",
                      (unsigned long)_playerLives];
        lives.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
        lives.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeRight;
        lives.position = CGPointMake(self.frame.size.width,
                                     self.frame.size.height);
        [self addChild:lives];

        SKLabelNode *level = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        level.fontSize = 16;
        level.fontColor = [SKColor blackColor];
        level.name = @"LevelLabel";
        level.text = [NSString stringWithFormat:@"Level: %lu",
                      (unsigned long)_levelNumber];
        level.verticalAlignmentMode = SKLabelVerticalAlignmentModeTop;
        level.horizontalAlignmentMode = SKLabelHorizontalAlignmentModeLeft;
        level.position = CGPointMake(0, self.frame.size.height);
        [self addChild:level];
    }
    return self;
}

这三个方法的作用分别如下:
- sceneWithSize:levelNumber: :这是一个工厂方法,用于一次性创建关卡并设置关卡编号。
- initWithSize: :重写类的默认初始化方法,将控制权传递给 initWithSize:levelNumber: 方法,并传递关卡编号的默认值。
- initWithSize:levelNumber: :设置关卡场景的基本配置,包括设置实例变量的值、场景的背景颜色,以及创建并添加用于显示生命数和关卡编号的标签节点。

修改 GameViewController.m

GameViewController.m viewDidLoad 方法中进行以下修改:

- (void)viewDidLoad
{
    [super viewDidLoad];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    skView.showsFPS = YES;
    skView.showsNodeCount = YES;
    skView.ignoresSiblingOrder = YES;

    // Create and configure the scene.
    GameScene *scene = [GameScene sceneWithSize:self.view.frame.size
                                  levelNumber:1];

    // Present the scene.
    [skView presentScene:scene];
}

- (BOOL)prefersStatusBarHidden {
    return YES;
}

这里我们不再从场景文件加载场景,而是使用刚刚添加到 GameScene sceneWithSize:levelNumber: 方法来创建和初始化场景,并使其与 SKView 大小相同。由于视图和场景大小相同,因此不再需要设置场景的 scaleMode 属性。 prefersStatusBarHidden 方法返回 YES 可以使 iOS 状态栏在游戏运行时消失,这对于此类动作游戏通常是必要的。

玩家移动功能实现

接下来,我们将为游戏添加一些交互性,让玩家能够通过触摸屏幕移动对象。具体步骤如下:
1. 创建 PlayerNode :使用 Xcode 的文件菜单创建一个名为 PlayerNode 的新 Cocoa Touch 类,它是 SKNode 的子类。在 PlayerNode.m 文件中添加以下方法:

- (instancetype)init {
    if (self = [super init]) {
        self.name = [NSString stringWithFormat:@"Player %p", self];
        [self initNodeGraph];
    }
    return self;
}

- (void)initNodeGraph {
    SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
    label.fontColor = [SKColor darkGrayColor];
    label.fontSize = 40;
    label.text = @"v";
    label.zRotation = M_PI;
    label.name = @"label";

    [self addChild:label];
}

PlayerNode 本身不进行绘制,而是通过 init 方法设置一个子节点( SKLabelNode )来进行实际绘制。我们还为标签设置了旋转值,使其显示的小写字母 “v” 颠倒显示。
2. PlayerNode 添加到场景中 :在 GameScene.m 中导入 PlayerNode.h 头文件,并添加一个属性:

#import "GameScene.h"
#import "PlayerNode.h"

@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;

@end

然后在 initWithSize:levelNumber: 方法的末尾添加以下代码:

[self addChild:level];
_playerNode = [PlayerNode node];
_playerNode.position = CGPointMake(CGRectGetMidX(self.frame),
                                   CGRectGetHeight(self.frame) * 0.1);

[self addChild:_playerNode];

此时,构建并运行应用程序,你会看到玩家出现在屏幕下部中间位置。
3. 处理触摸事件 :在 GameScene.m touchesBegan:withEvent: 方法中添加以下逻辑:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    /* Called when a touch begins */

    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];
        }
    }
}

该代码使用屏幕下部五分之一区域的触摸位置作为玩家节点移动的目标位置。由于我们尚未定义 PlayerNode moveToward: 方法,因此需要在 PlayerNode.h 中声明该方法:

#import <SpriteKit/SpriteKit.h>

@interface PlayerNode : SKNode

// returns duration of future movement
- (void)moveToward:(CGPoint)location;

@end

然后在 PlayerNode.m 中实现该方法:

- (void)moveToward:(CGPoint)location {
    [self removeActionForKey:@"movement"];

    CGFloat distance = PointDistance(self.position, location);
    CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat duration = 2.0 * distance / screenWidth;

    [self runAction:[SKAction moveTo:location duration:duration]
          withKey:@"movement"];
}

该方法首先移除之前的移动动作,然后计算移动距离和所需时间,最后创建并运行一个移动动作。

几何计算函数实现

由于代码中使用了 PointDistance() 函数,但 Xcode 找不到该函数,因此我们需要创建一个新的头文件 Geometry.h 来实现一些简单的几何计算函数:

#ifndef TextShooter_Geometry_h
#define TextShooter_Geometry_h

// Takes a CGVector and a CGFLoat.
// Returns a new CGFloat where each component of v has been multiplied by m.
static inline CGVector VectorMultiply(CGVector v, CGFloat m) {
    return CGVectorMake(v.dx * m, v.dy * m);
}

// Takes two CGPoints.
// Returns a CGVector representing a direction from p1 to p2.
static inline CGVector VectorBetweenPoints(CGPoint p1, CGPoint p2) {
    return CGVectorMake(p2.x - p1.x, p2.y - p1.y);
}

// Takes a CGVector.
// Returns a CGFloat containing the length of the vector, calculated using
// Pythagoras' theorem.
static inline CGFloat VectorLength(CGVector v) {
    return sqrtf(powf(v.dx, 2) + powf(v.dy, 2));
}

// Takes two CGPoints. Returns a CGFloat containing the distance between them,
// calculated with Pythagoras' theorem.
static inline CGFloat PointDistance(CGPoint p1, CGPoint p2) {
    return sqrtf(powf(p2.x - p1.x, 2) + powf(p2.y - p1.y, 2));
}

#endif

PlayerNode.m 中导入该头文件:

#import "Geometry.h"

现在构建并运行应用程序,玩家的飞船会根据触摸位置移动。

添加摆动动画

为了让玩家的飞船移动更加生动,我们可以为其添加一个摆动动画。在 PlayerNode moveToward: 方法中添加以下代码:

- (void)moveToward:(CGPoint)location {
    [self removeActionForKey:@"movement"];
    [self removeActionForKey:@"wobbling"];

    CGFloat distance = PointDistance(self.position, location);
    CGFloat pixels = [UIScreen mainScreen].bounds.size.width;
    CGFloat duration = 2.0 * distance / pixels;

    [self runAction:[SKAction moveTo:location duration:duration]
            withKey:@"movement"];

    CGFloat wobbleTime = 0.3;
    CGFloat halfWobbleTime = wobbleTime * 0.5;
    SKAction *wobbling = [SKAction
                sequence:@[[SKAction scaleXTo:0.2 duration:halfWobbleTime],
                           [SKAction scaleXTo:1.0
                                duration:halfWobbleTime]
                             ]];
    NSUInteger wobbleCount = duration / wobbleTime;

    [self runAction:[SKAction repeatAction:wobbling count:wobbleCount]
            withKey:@"wobbling"];
}

这里我们首先移除之前的摆动动作,然后定义一个摆动时间,摆动动画由缩放飞船的宽度实现,先将其缩放到正常大小的 2/10,再恢复到原始大小。最后,根据移动时间计算摆动次数,并重复执行摆动动画。

通过以上步骤,我们完成了从场景定制到玩家交互的一系列操作,为游戏添加了基本的功能和交互性。你可以根据需要进一步扩展和优化这些功能,打造出更加精彩的游戏。

总结

本文详细介绍了使用 Sprite Kit 进行游戏开发的基本流程,包括场景定制、玩家移动功能实现、几何计算函数的使用以及摆动动画的添加。通过这些步骤,我们可以为游戏添加基本的交互性和视觉效果。以下是整个开发过程的流程图:

graph LR
    A[初始场景定制] --> B[清理代码]
    A --> C[添加属性和方法声明]
    A --> D[实现新方法]
    D --> E[修改 GameViewController.m]
    E --> F[玩家移动功能实现]
    F --> G[创建 PlayerNode 类]
    F --> H[将 PlayerNode 添加到场景中]
    F --> I[处理触摸事件]
    I --> J[实现 moveToward: 方法]
    J --> K[几何计算函数实现]
    K --> L[添加摆动动画]

同时,为了方便大家回顾,这里总结了主要的代码文件和关键方法:
| 文件 | 关键方法 | 作用 |
| ---- | ---- | ---- |
| GameScene.h | sceneWithSize:levelNumber: initWithSize:levelNumber: | 创建和初始化场景,设置关卡编号和生命数 |
| GameScene.m | initWithSize:levelNumber: touchesBegan:withEvent: | 配置场景,处理触摸事件 |
| PlayerNode.h | moveToward: | 声明玩家节点的移动方法 |
| PlayerNode.m | init initNodeGraph moveToward: | 初始化玩家节点,设置子节点,实现移动和摆动动画 |
| Geometry.h | VectorMultiply VectorBetweenPoints VectorLength PointDistance | 实现几何计算函数 |

希望本文能帮助你更好地理解和掌握 Sprite Kit 的开发流程,让你能够开发出更加出色的游戏。

后续优化与拓展思路

在完成了基本的场景定制和玩家交互功能后,我们可以进一步对游戏进行优化和拓展,以提升游戏的趣味性和用户体验。以下是一些可以考虑的方向:

1. 碰撞检测

在游戏中,玩家的飞船可能会与其他物体发生碰撞,例如敌人的飞船或者障碍物。为了实现碰撞检测,我们可以使用 Sprite Kit 提供的物理引擎。具体步骤如下:

  • 为节点添加物理体 :在 PlayerNode initNodeGraph 方法中,为标签节点添加物理体。
- (void)initNodeGraph {
    SKLabelNode *label = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
    label.fontColor = [SKColor darkGrayColor];
    label.fontSize = 40;
    label.text = @"v";
    label.zRotation = M_PI;
    label.name = @"label";

    // 添加物理体
    label.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:label.frame.size];
    label.physicsBody.dynamic = YES;
    label.physicsBody.categoryBitMask = 0x01; // 玩家节点的类别掩码
    label.physicsBody.contactTestBitMask = 0x02; // 检测与敌人节点的碰撞

    [self addChild:label];
}
  • 实现碰撞检测代理方法 :在 GameScene 中实现 SKPhysicsContactDelegate 协议,并设置场景的物理世界代理。
@interface GameScene () <SKPhysicsContactDelegate>

@property (strong, nonatomic) PlayerNode *playerNode;

@end

@implementation GameScene

- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    if (self = [super initWithSize:size]) {
        // ... 其他初始化代码 ...

        self.physicsWorld.contactDelegate = self;
    }
    return self;
}

- (void)didBeginContact:(SKPhysicsContact *)contact {
    SKPhysicsBody *firstBody, *secondBody;

    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    } else {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }

    // 处理碰撞事件
    if ((firstBody.categoryBitMask & 0x01) != 0 && (secondBody.categoryBitMask & 0x02) != 0) {
        // 玩家节点与敌人节点发生碰撞
        NSLog(@"Player collided with enemy!");
        // 可以在这里添加减少玩家生命数、播放碰撞音效等逻辑
    }
}

@end
2. 关卡设计

目前游戏只有一个简单的关卡,我们可以设计多个不同难度的关卡,增加游戏的挑战性。具体实现步骤如下:

  • 定义关卡数据 :在 GameScene 中添加一个数组来存储不同关卡的信息,例如敌人的数量、出现的位置等。
@interface GameScene ()

@property (strong, nonatomic) PlayerNode *playerNode;
@property (strong, nonatomic) NSArray *levels;

@end

@implementation GameScene

- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    if (self = [super initWithSize:size]) {
        // ... 其他初始化代码 ...

        // 定义关卡数据
        self.levels = @[
            @{
                @"enemyCount": @3,
                @"enemyPositions": @[
                    [NSValue valueWithCGPoint:CGPointMake(100, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(200, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(300, 300)]
                ]
            },
            @{
                @"enemyCount": @5,
                @"enemyPositions": @[
                    [NSValue valueWithCGPoint:CGPointMake(50, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(150, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(250, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(350, 300)],
                    [NSValue valueWithCGPoint:CGPointMake(450, 300)]
                ]
            }
        ];

        // 根据关卡编号加载关卡
        [self loadLevel:levelNumber];
    }
    return self;
}

- (void)loadLevel:(NSUInteger)levelNumber {
    if (levelNumber > self.levels.count) {
        return;
    }

    NSDictionary *levelData = self.levels[levelNumber - 1];
    NSNumber *enemyCount = levelData[@"enemyCount"];
    NSArray *enemyPositions = levelData[@"enemyPositions"];

    // 创建敌人节点
    for (NSInteger i = 0; i < enemyCount.integerValue; i++) {
        NSValue *positionValue = enemyPositions[i];
        CGPoint position = [positionValue CGPointValue];

        // 创建敌人节点
        SKLabelNode *enemy = [SKLabelNode labelNodeWithFontNamed:@"Courier"];
        enemy.fontColor = [SKColor redColor];
        enemy.fontSize = 40;
        enemy.text = @"X";
        enemy.position = position;

        // 添加物理体
        enemy.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:enemy.frame.size];
        enemy.physicsBody.dynamic = YES;
        enemy.physicsBody.categoryBitMask = 0x02; // 敌人节点的类别掩码
        enemy.physicsBody.contactTestBitMask = 0x01; // 检测与玩家节点的碰撞

        [self addChild:enemy];
    }
}

@end
3. 音效和动画效果

为了增强游戏的氛围,我们可以添加音效和更多的动画效果。例如,在玩家飞船移动和碰撞时播放音效,为敌人的出现和消失添加动画效果。

  • 添加音效 :使用 SKAudioNode 来播放音效。在 GameScene initWithSize:levelNumber: 方法中添加以下代码:
- (instancetype)initWithSize:(CGSize)size levelNumber:(NSUInteger)levelNumber {
    if (self = [super initWithSize:size]) {
        // ... 其他初始化代码 ...

        // 添加移动音效
        SKAudioNode *moveSound = [SKAudioNode audioNodeWithFileNamed:@"move_sound.wav"];
        [self addChild:moveSound];

        // 添加碰撞音效
        SKAudioNode *collisionSound = [SKAudioNode audioNodeWithFileNamed:@"collision_sound.wav"];
        [self addChild:collisionSound];
    }
    return self;
}

PlayerNode moveToward: 方法中播放移动音效:

- (void)moveToward:(CGPoint)location {
    [self removeActionForKey:@"movement"];
    [self removeActionForKey:@"wobbling"];

    CGFloat distance = PointDistance(self.position, location);
    CGFloat pixels = [UIScreen mainScreen].bounds.size.width;
    CGFloat duration = 2.0 * distance / pixels;

    [self runAction:[SKAction moveTo:location duration:duration]
            withKey:@"movement"];

    // 播放移动音效
    SKAudioNode *moveSound = [self childNodeWithName:@"moveSound"];
    if (moveSound) {
        [moveSound runAction:[SKAction play]];
    }

    // ... 摆动动画代码 ...
}

GameScene didBeginContact: 方法中播放碰撞音效:

- (void)didBeginContact:(SKPhysicsContact *)contact {
    // ... 碰撞检测代码 ...

    // 播放碰撞音效
    SKAudioNode *collisionSound = [self childNodeWithName:@"collisionSound"];
    if (collisionSound) {
        [collisionSound runAction:[SKAction play]];
    }
}
  • 添加更多动画效果 :除了摆动动画,我们还可以为敌人的出现和消失添加动画效果。例如,在 loadLevel: 方法中为敌人节点添加淡入动画:
- (void)loadLevel:(NSUInteger)levelNumber {
    // ... 加载关卡代码 ...

    // 创建敌人节点
    for (NSInteger i = 0; i < enemyCount.integerValue; i++) {
        // ... 创建敌人节点代码 ...

        // 添加淡入动画
        enemy.alpha = 0;
        [enemy runAction:[SKAction fadeInWithDuration:0.5]];

        [self addChild:enemy];
    }
}

总结与展望

通过以上的优化和拓展,我们可以让游戏更加丰富和有趣。碰撞检测增加了游戏的挑战性,关卡设计让游戏有了更多的变化,音效和动画效果则提升了游戏的氛围和用户体验。以下是整个优化和拓展过程的流程图:

graph LR
    A[碰撞检测] --> B[为节点添加物理体]
    A --> C[实现碰撞检测代理方法]
    D[关卡设计] --> E[定义关卡数据]
    D --> F[加载关卡]
    G[音效和动画效果] --> H[添加音效]
    G --> I[添加更多动画效果]

同时,为了方便大家回顾,这里总结了新增的代码文件和关键方法:
| 文件 | 关键方法 | 作用 |
| ---- | ---- | ---- |
| GameScene.h | 无 | 无 |
| GameScene.m | loadLevel: didBeginContact: | 加载关卡,处理碰撞事件 |
| PlayerNode.m | moveToward: | 播放移动音效 |

未来,我们还可以进一步拓展游戏的功能,例如添加武器系统、升级系统、排行榜等,让游戏更加完善。希望这些思路能帮助你开发出更加出色的游戏。

考虑柔性负荷的综合能源系统低碳经济优化调度【考虑碳交易机制】(Matlab代码实现)内容概要:本文围绕“考虑柔性负荷的综合能源系统低碳经济优化调度”展开,重点研究在碳交易机制下如何实现综合能源系统的低碳化与经济性协同优化。通过构建包含风电、光伏、储能、柔性负荷等多种能源形式的系统模型,结合碳交易成本与能源调度成本,提出优化调度策略,以降低碳排放并提升系统运行经济性。文中采用Matlab进行仿真代码实现,验证了所提模型在平衡能源供需、平抑可再生能源波动、引导柔性负荷参与调度等方面的有效性,为低碳能源系统的设计与运行提供了技术支撑。; 适合人群:具备一定电力系统、能源系统背景,熟悉Matlab编程,从事能源优化、低碳调度、综合能源系统等相关领域研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①研究碳交易机制对综合能源系统调度决策的影响;②实现柔性负荷在削峰填谷、促进可再生能源消纳中的作用;③掌握基于Matlab的能源系统建模与优化求解方法;④为实际综合能源项目提供低碳经济调度方案参考。; 阅读建议:建议读者结合Matlab代码深入理解模型构建与求解过程,重点关注目标函数设计、约束条件设置及碳交易成本的量化方式,可进一步扩展至多能互补、需求响应等场景进行二次开发与仿真验证。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值