游戏引擎篇(一)-苹果2D引擎SpriteKit

最近研究了苹果自家开发的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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值