Sprite Kit 开发入门:从场景定制到玩家交互
在使用 Sprite Kit 进行游戏开发时,选择合适的缩放模式至关重要,它取决于应用程序的具体需求。若现有的缩放模式都不适用,还有另外两种选择:
1. 支持一组固定的屏幕尺寸,为每个尺寸创建单独的设计,并将其存储在各自的
.sks
文件中,在需要时从正确的文件加载场景。
2. 通过代码创建场景,使其与呈现它的
SKView
大小相同,并以编程方式填充节点。不过,这种方法仅适用于游戏元素的相对位置不依赖于精确值的情况。下面我们将以 TextShooter 应用程序为例,详细介绍这种方法的实现过程。
初始场景定制
-
清理代码
:打开 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
-
添加属性和方法声明
:由于我们不打算从
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
-
实现新方法
:切换到
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:
| 播放移动音效 |
未来,我们还可以进一步拓展游戏的功能,例如添加武器系统、升级系统、排行榜等,让游戏更加完善。希望这些思路能帮助你开发出更加出色的游戏。
超级会员免费看
490

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



