最近研究了苹果自家开发的2D引擎SpriteKit和3D引擎SceneKit,开篇之前,需要客观的讲,如果你要从事的是团队或者公司的项目,还是直接unity搞起,这涉及到开发与维护成本的问题,毕竟SpriteKit目前无法对跨平台给予支持。但是如果你是一个独立开发者,对苹果原生框架感兴趣,或者只关注与苹果的App Store,我想SpriteKit和SceneKit也是个不错的选择。
Sprite译作精灵,可以这样理解,在SpriteKit的世界里,游戏里的怪兽是一个精灵,主角与主角发射的炮弹也是一个精灵,或者说游戏里的一个不会动的背景图,也可以是一个精灵。下面以精灵为切入点,讲解一下一个充满野心的苹果弄出来的2D引擎。
一、精灵与场景
1.新建一个Xcode工程,可以看到,不管是iOS,还是macOS,甚至于tvOS,都有一个叫Game的项目新建方式。我们选择iOS的Game,新建出一个游戏项目。Game与普通工程项目有什么不同,其实就一点项目默认的GameViewController的view是以skView的形式load出来的,下面我们接着新建一个游戏场景SKScene,用以装载即将new出来的精灵。
2.场景的新建。
注:Scene场景的起始坐标是以左下角为(0,0)原点,而非传统view的左上角。
- (MenuScene *)menuScene {
if (!_menuScene) {
_menuScene = [[MenuScene alloc] initWithSize:self.view.bounds.size];
//@interface MenuScene : SKScene
//@end
}
return _menuScene;
}
- (void)viewDidLoad {
[super viewDidLoad];
[(SKView *)self.view presentScene:self.menuScene];
// Do any additional setup after loading the view, typically from a nib.
}
@implementation MenuScene
-(instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
#if TARGET_OS_IPHONE
#define SKColor UIColor
#else
#define SKColor NSColor
#endif
// SKColor主要是为了兼容mac的NSColor与iOS的UIColor
self.backgroundColor = [SKColor whiteColor];
[self addChild:self.titleNode];
[self addChild:self.pathLabelNode];
[self addChild:self.collLabelNode];
[self addChild:self.physLabelNode];
[self addChild:self.physCollNode];
}
return self;
}
3.游戏场景的切换。与初始化的页面一致,游戏的转场为使用presentScene跳转到新建的Scene当中。
- (SKLabelNode *)titleNode {
if (!_titleNode) {
_titleNode = ({
SKLabelNode *labelNode = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"];
labelNode.text = @"SpriteKit Test";
labelNode.fontSize = 30;
labelNode.fontColor = [SKColor blueColor];
labelNode.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height * 0.75);
labelNode.name = labelNode.text;
labelNode;
});
}
return _titleNode;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
[self changeToGameSceneWithNodeName:node.name];
}
}
-(void)changeToGameSceneWithNodeName:(NSString *)nodeName {
NSLog(@"nodeName=%@",nodeName);
if ([nodeName isEqualToString:self.pathLabelNode.name]) {
PathScene *pathScene = [PathScene sceneWithSize:self.size];
// 定制转场类型
SKTransition *reveal = [SKTransition revealWithDirection:SKTransitionDirectionUp duration:0.5];
[self.scene.view presentScene:pathScene transition:reveal];
}
else if ...
4.精灵的新建与添加。精灵的新建有两种形式,一个是直接以图片的形式新建,其size默认为图片的size,另一种则以纹理的形式新建。
// 以图片新建
- (SKSpriteNode *)player {
if (!_player) {
_player = [SKSpriteNode spriteNodeWithImageNamed:@"player"];
_player.name = @"player";
_player.position = CGPointMake(self.size.width /2, self.size.height /2);
}
return _player;
}
// 以纹理新建
- (SKSpriteNode *)walkMan {
if (!_walkMan) {
_walkMan = [SKSpriteNode spriteNodeWithImageNamed:@"walkR01"];
_walkMan.name = @"walkMan";
_walkMan.position = CGPointMake(self.player.position.x, CGRectGetMaxY(self.player.frame) + 30);
SKTexture * texture1 = [SKTexture textureWithImageNamed:@"walkR01"];
SKTexture * texture2 = [SKTexture textureWithImageNamed:@"walkR02"];
SKTexture * texture3 = [SKTexture textureWithImageNamed:@"walkR03"];
SKTexture * texture4 = [SKTexture textureWithImageNamed:@"walkR04"];
SKTexture * texture5 = [SKTexture textureWithImageNamed:@"walkR05"];
SKAction *animation = [SKAction animateWithTextures:@[texture1, texture2, texture3, texture4, texture5] timePerFrame:1];
SKAction *action = [SKAction repeatActionForever:animation];
[_walkMan runAction:action];
}
return _walkMan;
}
5、精灵的添加运动事件
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
// 添加武器
SKSpriteNode * arms = [SKSpriteNode spriteNodeWithImageNamed:@"projectile"];
arms.position = self.player.position;
[self addChild:arms];
CGPoint location = [touch locationInNode:self];
// 直线轨迹
// SKAction * moveToAction = [SKAction moveTo:location duration:0.5];;
// 持续增加
// SKAction * moveByAction = [SKAction moveByX:100 y:100 duration:0.3];
// 改变大小
// SKAction * sizeAction = [SKAction resizeByWidth:arms.size.width * 1.5 height:arms.size.height * 1.5 duration:0];
// 旋转
// SKAction * radiansAction = [SKAction rotateByAngle:M_PI * 4 duration:moveToAction.duration];
// 音效
// SKAction * armsSound = [SKAction playSoundFileNamed:@"pew-pew-lei.caf" waitForCompletion:NO];
SKAction * armsAction = [SKAction group:@[pathAction, sizeAction,armsSound]];
[arms runAction:armsAction completion:^{
// 移除
[arms removeFromParent];
}];
}
}
二、精灵的接触检测
SKScene有一个场景方法,改方法每帧都会触发一次,可供简单的事件分析与监测,比如精灵越界销毁,精灵的接触监测,故事板的得分情况的更新等等。
/**
Override this to perform per-frame game logic. Called exactly once per frame before any actions are evaluated and any physics are simulated.
@param currentTime the current time in the app. This must be monotonically increasing.
*/
- (void)update:(NSTimeInterval)currentTime;
-(void)update:(CFTimeInterval)currentTime {
// 怪物与武器的越界移除
// 更新数字版
// 检测精灵事件(如点击精灵之后,给它设置个标识,在下一帧的时候做事件处理)
}
注:后面讲述精灵的物理碰撞,能够更准确的进行精灵的碰撞检测
三、精灵的物理引擎
// 方形
- (SKSpriteNode *)square {
if (!_square) {
_square = [SKSpriteNode spriteNodeWithImageNamed:@"square"];
_square.position = CGPointMake(self.size.width * 0.8, CGRectGetMidX(self.frame));
_square.name = @"square_prey";
_square.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_square.size];
}
return _square;
}
// 圆形
- (SKSpriteNode *)circle {
if (!_circle) {
_circle = [SKSpriteNode spriteNodeWithImageNamed:@"circle"];
_circle.position = CGPointMake(self.size.width * 0.65, CGRectGetMidX(self.frame));
_circle.name = @"circle_prey";
_circle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:_circle.size.width / 2];
}
return _circle;
}
// 三角形
- (SKSpriteNode *)triangle {
if (!_triangle) {
_triangle = [SKSpriteNode spriteNodeWithImageNamed:@"triangle"];
_triangle.position = CGPointMake(self.size.width * 0.5, CGRectGetMidX(self.frame));
_triangle.name = @"triangle_prey";
CGMutablePathRef trianglePath = CGPathCreateMutable();
// 中心
CGPathMoveToPoint(trianglePath, nil, -_triangle.size.width / 2, -_triangle.size.height / 2);
//
CGPathAddLineToPoint(trianglePath, nil, _triangle.size.width / 2, -_triangle.size.height / 2);
CGPathAddLineToPoint(trianglePath, nil, 0, _triangle.size.height / 2);
CGPathAddLineToPoint(trianglePath, nil, -_triangle.size.width / 2, -_triangle.size.height / 2);
_triangle.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:trianglePath];
CGPathRelease(trianglePath);
}
return _triangle;
}
四、精灵的物理碰撞检测
-(instancetype)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [UIColor whiteColor];
[self addChild:self.back];
[self addChild:self.square];
[self addChild:self.circle];
[self addChild:self.triangle];
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame];
self.scene.name = @"self";
self.physicsBody.categoryBitMask = 0x00000001;
self.physicsBody.collisionBitMask = 0x00000001;
self.physicsBody.contactTestBitMask = 0x00000001;
self.physicsWorld.contactDelegate = (id <SKPhysicsContactDelegate>)self;
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
// 获取点击的SKNode
SKNode * node = [self nodeAtPoint:location];
// 新建一个黑球
Node * ball = [SKSpriteNode spriteNodeWithImageNamed:@"blackBall"];
ball.position = CGPointMake(0, 0);
ball.name = @"ball";
ball.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:ball.size.width / 2];
CGPoint offset = CGPointMake(location.x - ball.position.x, location.y - ball.position.y);
// 斜率
float ratio = (float) offset.y / (float) offset.x;
// 速度
ball.physicsBody.velocity = CGVectorMake(1000, 1000 * ratio);
// 角速度 弧度/秒
ball.physicsBody.angularVelocity = M_PI * 4;
// 密度
ball.physicsBody.density = 100;
// 弹力
ball.physicsBody.restitution = 1;
// 动量 /kg
ball.physicsBody.mass = 100;
// 光滑度 0 ~ 1
ball.physicsBody.friction = 0.5;
// 是否受重力影响 default value is YES
ball.physicsBody.affectedByGravity = NO;
// 是否受加速度影响
ball.physicsBody.allowsRotation = NO;
// 线性阻尼(0:速度从不减弱;1:速度立即减弱)
ball.physicsBody.linearDamping = 0.5;
// 角速度阻尼(0:速度从不减弱;1:速度立即减弱)default 0.1
ball.physicsBody.angularDamping = 0;
// 物体的类别(一个16进制数)
ball.physicsBody.categoryBitMask = 0x00000001;
// 设置哪个物体不可与之碰撞(即不可穿透)
ball.physicsBody.collisionBitMask = 0x00000001;
// 接触(触发检测函数)
ball.physicsBody.contactTestBitMask = 0x00000001;
[self addChild:ball];
}
}
- (void)didBeginContact:(SKPhysicsContact *)contact {
NSLog(@"联系中的第一个物体:%@",contact.bodyA.node.name);
NSLog(@"联系中的第二个物体:%@",contact.bodyB.node.name);
NSLog(@"联系点的坐标:%@",NSStringFromCGPoint(contact.contactPoint));
NSLog(@"碰撞方向的法向量:%@",NSStringFromCGVector(contact.contactNormal));
NSLog(@"两个物体的碰撞强度(牛顿每秒):%f",contact.collisionImpulse);
if ([contact.bodyA.node.name containsString:@"prey"] && [contact.bodyB.node.name isEqualToString:@"ball"]) {
[contact.bodyA.node removeFromParent];
}
if ([contact.bodyB.node.name containsString:@"prey"] && [contact.bodyA.node.name isEqualToString:@"ball"]) {
[contact.bodyB.node removeFromParent];
}
}
@end