ios開發中蘋果2D引擎SpriteKit介紹,iosspritekit
ios開發中蘋果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 )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