最近用闲暇时间了解了下iOS 7.0 SDK就提供的一个2D游戏引擎框架SpriteKit,用此实现了一个之前比较流行的游戏“保卫萝卜”中的一个小场景,毕竟有具体需求的实践能提高学习效率,在此做个记录总结。

SpriteKit主要用来做2D游戏,将图形渲染和动画用于任意一个精灵,游戏中的任意一个图像元素都可以理解为一个精灵,我们要做的就是通过SpriteKit来管理这些精灵,让它们像我们预想的方向展现或变化。

同样SDK还提供了3D渲染引擎SceneKit,和GPU优化的底层接口来渲染3D图形的API Metal,这些之后再来学习。

先预览一下实现后的小游戏。

保卫萝卜

一、工程搭建

游戏的工程搭建我们可以直接创建一个XCode提供的Game Project模板,也可以直接在已有的工程中使用。

1、Game 模板

在创建工程处选择Game模板,路径File->New->Project->Game,样式见下图。

Game模板

Language选择你喜欢用的Objective-C或Swift,Game Technology这里选择SpriteKit。创建完成后目录结构如下。

模板目录结构

工程会默认生成一个“Hello,World”的Demo,运行起来可以直接看到效果,按下、抬起、移动手指的交互动作都做了动画,可以感受下。

根视图GameViewController中,调用了GameScene,Hello Wold所有的精灵展现及动画都是在GameScene中处理的。所以想要开始开发游戏之前,需要先将这个Hello World Demo相关内容清理掉。视图文件GameScene.sks、动画文件Actions.sks、源码文件GameScene.h、GameScene.m都删除,并将GameViewController中的viewDidLoad函数中创建并使用GameScene的代码也相应删除掉。这样就可以在这个GameViewController中创建并使用我们要做的Scene了。

2、已有的工程中开发游戏

在想要实现游戏的界面,将Storyboard或Xib中ViewController的根View,Class类由UIView改成SKView,因为游戏框架的元素都要使用SpriteKit提供的类来实现,方便管理精灵。这样就跟Game模型中的GameViewController一样了。

二、开始使用

1、创建Scene文件并在ViewController中调用

创建MyScene文件,继承SKScene,后续游戏的内容都将在这个页面开发。# MyScene.h

import

@interface MyScene : SKScene

@end# MyScene.m

#import "GameScene.h"

@implementation GameScene

@end

在游戏页面的ViewController中,创建一个SKView,用于展现才刚创建的MyScene。IBOutlet SKView *_sceneView;

在Xib或Storyboard中添加UIView,将类改为SKView,关联即可。

在ViewController中的viewDidLoad中调用MyScene。// Create and configure the scene.

CGSize size = CGSizeMake(_sceneView.bounds.size.height, _sceneView.bounds.size.width);

MyScene * scene = [MyScene sceneWithSize:size];

scene.scaleMode = SKSceneScaleModeAspectFill;

// Present the scene.

[_sceneView presentScene:scene];

MyScene已经调用完成,后续精灵的展现、动画都在MyScene文件里添加。

2、在MyScene中添加精灵// 添加背景

- (void)addBackground {

SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:@"bg"];

background.name = @"bg";

background.size = CGSizeMake(self.size.width, self.size.height);

background.position = CGPointMake(background.size.width/2, background.size.height/2);

[self addChild:background];

}

基本图片类的精灵都是通过SKSpriteNode对象来创建,通过spriteNodeWithImageNamed:函数可以将图片做成精灵对象,通过name属性为其命名,用户点击等操作可以通过名字判断出是哪个精灵,后面会介绍到。通过size和postion属性可以指定精灵的宽高和位置。

同样的方法将游戏中用到的一些静态精灵都摆放上去。- (id)initWithSize:(CGSize)size{

if (self = [super initWithSize:size]) {

self.backgroundColor = [SKColor whiteColor];

[self addBackground];    // 背景

[self addFlag];                // 起点

[self addCarrot];             // 终点

[self addStaticObject];    // 其他的羊头、石头、宝箱等装饰物

}

return self;

}

SKColor在iOS上就是UIColor,可以看下定义,是为了在MacOS上和iOS上统一使用,也方便移植。#if TARGET_OS_IPHONE

#define SKColor UIColor

#else

#define SKColor NSColor

#endif

添加完这些精灵的样式如图。

九个精灵

3、为精灵添加动画这里要涉及到SKAction,简单介绍一下,因为游戏的大部分动作行为都是它来实现的。

SKAction可以实现的动作有很多,列举几个常用的

1、相对位移或绝对位移

2、旋转到指定角度或旋转指定角度

3、改变尺寸或缩放

4、隐藏、显示、渐隐、渐现、指定透明度

5、添加一个或一系列纹理图片

6、加减速、等待

7、播放简单的声音等等

以上是单个动作的SKAction,同样可以通过下面两个函数将多个单一动作整合成复合动作SKAction

+ (SKAction *)sequence:(NSArray *)actions;

+ (SKAction *)group:(NSArray *)actions;

入参是N多个Action,sequence:函数是串行的来执行数组中的所有Action,group:函数是并行的来执行。通过以上这些我们就可以做各种复杂的动画了。

1、首先通过SKAction来为仙人掌加一个张嘴呼吸的动画。NSArray *array = @[[SKTexture textureWithImageNamed:@"s1"],

[SKTexture textureWithImageNamed:@"s2"],

[SKTexture textureWithImageNamed:@"s3"],

[SKTexture textureWithImageNamed:@"s4"],

[SKTexture textureWithImageNamed:@"s5"]];

SKAction *animation = [SKAction animateWithTextures:array timePerFrame:0.15];

// cactus为仙人掌的SKSpriteNode对象

[cactus runAction:[SKAction repeatActionForever:animation]];

SKAction的animateWithTextures:timePerFrame:函数可以将一组图片按指定的时间间隔像GIF图片一样展示,类似于UIImageView中的animationImages。SKAction的repeatActionForever:函数将GIF的动作无限循环,做成复合动画。精灵通过运行runAction:函数调用它就可以实现仙人掌呼吸动画了。

2、再来给终点的萝卜增加点击交互,点击后抖动和叫声。

因为SKScene继承自UIResponder,所以我们可以使用touchesBegan:withEvent:函数来实现点击- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

for (UITouch *touch in touches) {

CGPoint touchLocation = [touch locationInNode:self];

SKNode *node = [self nodeAtPoint:touchLocation];

if ([node.name isEqualToString:@"carrot"] && ![node hasActions]) {

[self carrotAnimation];

}

}

}

上面代码用来判断点击位置是否为萝卜精灵,如果是并且没有在做动作,就执行下面的Action。- (void)carrotAnimation{

// 播放一组图片

NSArray *array = @[[SKTexture textureWithImageNamed:@"luobo2"],

[SKTexture textureWithImageNamed:@"luobo3"],

[SKTexture textureWithImageNamed:@"luobo4"],

[SKTexture textureWithImageNamed:@"luobo5"],

[SKTexture textureWithImageNamed:@"luobo6"],

[SKTexture textureWithImageNamed:@"luobo7"],

[SKTexture textureWithImageNamed:@"luobo8"],

[SKTexture textureWithImageNamed:@"luobo9"],

[SKTexture textureWithImageNamed:@"luobo1"]];

SKAction *animation = [SKAction animateWithTextures:array timePerFrame:0.05 resize:YES restore:NO];

// 随机播放一个声音

int random = arc4random();

NSString *str = @"carrot1.mp3";

if (random % 3 == 1) { str = @"carrot2.mp3"; }

else if (random % 3 == 2) { str = @"carrot3.mp3"; }

SKAction *soundAction = [SKAction playSoundFileNamed:str waitForCompletion:NO];

SKAction *groupAction = [SKAction group:@[animation, soundAction]];

[self.carrot runAction:groupAction];

}

为萝卜做了一个GIF和一个随机声音动作,通过group合成一个Action,让萝卜执行。

3、小怪物的出现和行走路径

在起始位置创建一个小怪物,并伴随着一个叫声。// 小怪物

SKSpriteNode *character = [SKSpriteNode spriteNodeWithImageNamed:@"player1"];

character.position = CGPointMake(60 + character.size.width/2, self.size.height - character.size.height/2);

[self addChild:character];

// 叫声

SKAction *soundAction = [SKAction playSoundFileNamed:@"MC.mp3" waitForCompletion:NO];

[character runAction:soundAction];

同时出现一个漩涡,漩涡旋转两圈并消失。// 出现漩涡

SKSpriteNode *wheel = [SKSpriteNode spriteNodeWithImageNamed:@"wheel"];

wheel.position = CGPointMake(character.position.x, character.position.y - 10);

[self addChild:wheel];

// 旋转两圈后消失

SKAction *rotateAction = [SKAction rotateByAngle:4 * M_PI duration:0.4];

[wheel runAction:rotateAction completion:^{

[wheel removeFromParent];

}];

小怪物蹦蹦跳跳的动画,与仙人掌呼吸动画一样。NSArray *array = @[[SKTexture textureWithImageNamed:@"player1"],

[SKTexture textureWithImageNamed:@"player2"],

[SKTexture textureWithImageNamed:@"player3"],

[SKTexture textureWithImageNamed:@"player1"]];

SKAction *animation = [SKAction animateWithTextures:array timePerFrame:0.15];

[character runAction:[SKAction repeatActionForever:animation]];

按照固定路线行走。(正常需要根据路线、屏幕尺寸等计算出,这里给的定值)CGMutablePathRef pathRef = CGPathCreateMutable();

CGPathMoveToPoint(pathRef, nil, character.position.x, character.position.y);

CGPathAddLineToPoint(pathRef, nil, 83, 165);

CGPathAddLineToPoint(pathRef, nil, 245, 165);

CGPathAddLineToPoint(pathRef, nil, 245, 225);

CGPathAddLineToPoint(pathRef, nil, 415, 225);

CGPathAddLineToPoint(pathRef, nil, 415, 160);

CGPathAddLineToPoint(pathRef, nil, 580, 160);

CGPathAddLineToPoint(pathRef, nil, 580, self.size.height - character.size.height/2);

SKAction *moveToEndAction = [SKAction followPath:pathRef asOffset:NO orientToPath:NO duration:10];

[character runAction:moveToEndAction completion:^{

// 走完全程后,移除小怪物

[character removeFromParent];

[self.monsters removeObject:character];

}];

// 将小怪物暂存起来,便于后续跟炮台的飞镖做碰撞时使用。

[self.monsters addObject:character];

CGPathRelease(pathRef);

这里通过followPath:asOffset:orientToPath:duration:函数来让精灵按CGPathRef路线移动10秒。self.monsters这个成员变量是用来存储每一个生成出来的小怪物,如果小怪物走完全程也要相应的移除掉。

我们来连续的创建这样的小怪物7个,时间间隔1秒。typeof(self) weakSelf = self;

SKAction *actionWaitNextMonster = [SKAction waitForDuration:1];

SKAction *actionAddMonster = [SKAction runBlock:^{

// 添加小怪物的所有动作行为,出场、叫声、行动路线等

[weakSelf addMonster];

}];

SKAction *sequenceAction = [SKAction sequence:@[actionAddMonster, actionWaitNextMonster]];

[self runAction:[SKAction repeatAction:sequenceAction count:7] completion:^{

// isFinish用来标记怪物全部出场

weakSelf.isFinsh = YES;

}];

4、炮台和飞镖的展现

添加点击交互,点击背景,会在相应的位置上放上炮台,逻辑与点击萝卜的思路一样。通过touchesBegan:withEvent:函数获取点击事件,判定是否点击在背景上,并且捕获到点击的位置,在该位置上添加炮台。#pragma mark - UIResponder

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{

for (UITouch *touch in touches) {

CGPoint touchLocation = [touch locationInNode:self];

SKNode *node = [self nodeAtPoint:touchLocation];

if ([node.name isEqualToString:@"carrot"] && ![node hasActions]) {

[self carrotAnimation]; // 点击萝卜

}

else if ([node.name isEqualToString:@"bg"] && ![node hasActions]) {

[self addArrow:touchLocation]; // 点击背景

}

}

}

创建炮台、飞镖和飞镖移动的逻辑,因为炮台由底座和飞镖两层图片组成,所以这里使用addChild:函数来叠加使用,在炮台出现时再添加个声音 。- (void)addArrow:(CGPoint)point{

// 两张图片组装成炮台精灵

SKSpriteNode *character1 = [SKSpriteNode spriteNodeWithImageNamed:@"rotate1"];

character1.position = point;

SKSpriteNode *character2 = [SKSpriteNode spriteNodeWithImageNamed:@"rotate2"];

[character1 addChild:character2];

[self addChild:character1];

// 放置炮台时添加个声音

SKAction *soundAction = [SKAction playSoundFileNamed:@"Select.mp3" waitForCompletion:NO];

[self runAction:soundAction];

// 不断创建飞镖,撇向小怪物的动作

SKAction *actionAdd = [SKAction runBlock:^{

SKSpriteNode *character3 = [SKSpriteNode spriteNodeWithImageNamed:@"rotate2"];

character3.position = point;

[self addChild:character3];

if (self.monsters.count > 0) {

SKSpriteNode *monster = [self.monsters objectAtIndex:0];

CGPoint point = monster.position;

SKAction *rotateAction = [SKAction rotateByAngle: - 2 * M_PI duration:0.1];

SKAction *moveAction = [SKAction moveTo:CGPointMake(point.x, point.y) duration:1.0];

[character3 runAction:moveAction completion:^{

[character3 removeFromParent];

[self.projectiles removeObject:character3];

}];

[character3 runAction:[SKAction repeatActionForever:rotateAction]];

}

[self.projectiles addObject:character3];

}];

if (self.monsters.count > 0) {

SKAction *actionWaitNext = [SKAction waitForDuration:1];

SKAction *soundAction = [SKAction playSoundFileNamed:@"Arrow.mp3" waitForCompletion:NO];

SKAction *groupAction = [SKAction group:@[actionAdd, soundAction]];

[self runAction:[SKAction repeatActionForever:[SKAction sequence:@[groupAction, actionWaitNext]]]];

}

}

self.projectiles这个成员变量是用来存储场中的飞镖,后续阶段用它来判定是否有打到小怪物。和之前用来存储小怪物的变量self.monsters类似。

4、碰撞或相交的事件处理

这里涉及到SKScene的每一帧的处理循环,下图是苹果SDK SKScene 类中提供的。

Frame processing in a scene// 官方解读

1、The scene’s update: method is called with the time elapsed so far in the simulation. This is the primary place to implement your own in-game simulation, including input handling, artificial intelligence, game scripting, and other similar game logic. Often, you use this method to make changes to nodes or to run actions on nodes.

2、The scene processes actions on all the nodes in the tree. It finds any running actions and applies those changes to the tree. In practice, because of custom actions, you can also hook into the action mechanism to call your own code. You cannot directly control the order in which actions are processed or cause the scene to skip actions on certain nodes, except by removing the actions from those nodes or removing the nodes from the tree.

3、The scene’s didEvaluateActions method is called after all actions for the frame have been processed.

4、The scene simulates physics on nodes in the tree that have physics bodies. Adding physics to nodes in a scene is described in SKPhysicsBody, but the end result of simulating physics is that the position and rotation of nodes in the tree may be adjusted by the physics simulation. Your game can also receive callbacks when physics bodies come into contact with each other, see SKPhysicsContactDelegate.

5、The scene’s didSimulatePhysics method is called after all physics for the frame has been simulated.

6、The scene applies any constraints associated with nodes in the scene. Constraints are used to establish relationships in the scene. For example, you can apply a constraint that makes sure a node is always pointed at another node, regardless of how it is moved. By using constraints, you avoid needing to write a lot of custom code in your scene handling.

7、The scene calls its didApplyConstraints method.

8、The scene calls its didFinishUpdate method. This is your last chance to make changes to the scene.

9、The scene is rendered.

这里用到了第一步update:函数,通过CGRectIntersectsRect函数判断两个精灵是否相交,来加入相应的Action。以下是实现update:函数,并写在函数体中的。(这里也可以用碰撞引擎方案来替换)

1、判定飞镖打中小怪物NSMutableArray *projectilesToDelete = [[NSMutableArray alloc] init];

for (SKSpriteNode *projectile in self.projectiles) {

NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init];

// 判定是否有飞镖打中小怪物

for (SKSpriteNode *monster in self.monsters) {

if (CGRectIntersectsRect(projectile.frame, monster.frame)) {

[monstersToDelete addObject:monster];

}

}

// 移除小怪物

for (SKSpriteNode *monster in monstersToDelete) {

[self.monsters removeObject:monster];

[monster removeFromParent];

int random = arc4random();

NSString *str = (random % 2 == 0) ? @"Fly162.mp3" : @"Fat242.mp3";

SKAction *soundAction = [SKAction playSoundFileNamed:str waitForCompletion:NO];

[self runAction:soundAction];

// 该函数为添加小怪物消失时的动画,一个气泡破裂的GIF,就不贴代码了

[self addcloud:CGPointMake(monster.position.x, monster.position.y)];

}

if (monstersToDelete.count > 0) {

// 飞镖打中小怪物,飞镖也应该消失,先暂存下

[projectilesToDelete addObject:projectile];

}

}

// 移除飞镖

for (SKSpriteNode *projectile in projectilesToDelete) {

[self.projectiles removeObject:projectile];

[projectile removeFromParent];

}

2、判定小怪物吃到萝卜NSMutableArray *monstersToDelete = [[NSMutableArray alloc] init];

for (SKSpriteNode *monster in self.monsters)    {

if (CGRectIntersectsRect(monster.frame, self.carrot.frame)) {

[monstersToDelete addObject:monster];

[self.carrot removeAllActions];

SKAction *soundAction = [SKAction playSoundFileNamed:@"Crash.mp3" waitForCompletion:NO];

[self runAction:soundAction];

passMonsterCount ++;         // 记录被咬了几口

NSArray *imageNameArray = @[@"cry1", @"cry2", @"cry3", @"cry4", @"cry5", @"cry6"];

NSString *imageNamed = imageNameArray[passMonsterCount-1];

if (imageNamed.length > 0) {

SKTexture *cryTexture = [SKTexture textureWithImageNamed:imageNamed];

SKAction *action = [SKAction setTexture:cryTexture resize:YES];

[self.carrot runAction:action];

}

}

}

// 移除咬萝卜的小怪物

for (SKSpriteNode *monster in monstersToDelete)    {

[self.monsters removeObject:monster];

[monster removeFromParent];

}

if (passMonsterCount >= 6) {

// 失败!游戏结束!

SKAction *soundAction = [SKAction playSoundFileNamed:@"Lose.mp3" waitForCompletion:NO];

[self runAction:[SKAction sequence:@[[SKAction waitForDuration:0.5], soundAction]] completion:^{

[self setPaused:YES];

}];

}

else if (self.isFinsh && [self.monsters count] == 0 && passMonsterCount < 6) {

// 胜利!游戏结束!

SKAction *soundAction = [SKAction playSoundFileNamed:@"Perfect.mp3" waitForCompletion:NO];

[self runAction:soundAction completion:^{

[self setPaused:YES];

}];

}

至此,一个简单场景下的小游戏就结束了,这里只是用于技术实现,如果作为游戏来讲,需要处理的很要很多,如点击背景放置炮台的避让策略,行走路径根据屏幕尺寸地图样式等如何计算获取,物理碰撞逻辑等等。

spritekit 动画_iOS 2D游戏引擎框架SpriteKit入门相关推荐

  1. 认识AndEngine选自Android 2D游戏引擎AndEngine快速入门教程

    认识AndEngine什么是AndEngine 随着Android手机.平板的盛行,Android下的游戏也不断的变得火热.而对于游戏开发有兴趣的同学们,应该也想要学习开发游戏.虽说游戏开发的引擎较多 ...

  2. Android 2D游戏引擎AndEngine快速入门教程

    Android 2D游戏引擎AndEngine快速入门教程 介绍:AndEngine是一款知名的Android 2D游戏引擎.该引擎代码开源,并且可以免费使用.本书详细讲解如何使用AndEngine引 ...

  3. Qt 2D游戏引擎QtGameEngine使用入门案例

    Qt Game Engine (QGE) 是一个用 C++ 编写的2D游戏引擎,构建在 Qt 框架之上.它提供了一个非常简单有趣的使用界面,用于从自上而下或有角度(例如等轴测)的角度创建您自己的 2d ...

  4. 开发2d游戏要用什么引擎_下一个游戏要使用什么2D游戏引擎

    开发2d游戏要用什么引擎 A few weeks ago, I posted about my experience attempting to make a prototype in a bunch ...

  5. Android 2D游戏引擎AndEngine配置环境

    Android 2D游戏引擎AndEngine配置环境 1.2  配置环境 在任何编程中,都需要一些软件或者硬件的支持.否则,没有硬件软件是不可能存在的,而想要编写对应语言的的程序,这需要对应语言库和 ...

  6. 转:高层游戏引擎——基于OGRE所实现的高层游戏引擎框架

    高层游戏引擎--基于OGRE所实现的高层游戏引擎框架 这是意念自己的毕业论文,在一个具体的实践之中,意念主要负责的是物件和GUI之外的其他游戏系统.意念才学疏陋,望众位前辈不吝赐教.由于代码质量不高. ...

  7. 高层游戏引擎——基于OGRE所实现的高层游戏引擎框架

    技术文档(Document) 来自:noslopforever的专栏 高层游戏引擎--基于OGRE所实现的高层游戏引擎框架 这是意念自己的毕业论文,在一个具体的实践之中,意念主要负责的是物件和GUI之 ...

  8. 2d游戏引擎_8年,从2D到3D,我的学习之路

    Mickey 写了一篇 <一个本科毕业生创业两年的感悟>,从他的视角,总结了我们合作的两年经历. 我也来写一篇,介绍我的学习之路,希望对大家有所帮助,谢谢大家- 我的学习方法 1.直接从0 ...

  9. 游戏开发心得——书籍篇——《游戏引擎框架》-导论

    游戏开发心得--书籍篇--<游戏引擎框架>-导论 FOR THE SIGMA FOR THE GTINDER FOR THE ROBOMASTER 简介: 学习<游戏引擎框架> ...

最新文章

  1. scikit-learn - 分类模型的评估 (classification_report)
  2. PAT(甲级)2018年冬季考试 7-2 Decode Registration Card of PAT
  3. 【读书笔记《Bootstrap 实战》】3.优化站点资源、完成响应式图片、让传送带支持手势...
  4. Cache多核之间的一致性MESI
  5. 【学术相关】发表 SCI 论文有哪些实用工具?
  6. React中的纯组件
  7. Python序列基本操作(三)
  8. TaskPaper教程——如何安装运行脚本?
  9. 用viewpager实现图片轮播
  10. 2017/12/30 GUI和动态代理
  11. DataList 编辑记录时,更新取不到值的原因。
  12. 大数据与机器学习:实践方法与行业案例.1.4 本章小结
  13. WINDOWS下获取目录环境变量的C代码
  14. 学计算机ppt感想60字,ppt制作的体会和感受
  15. 中芯国际公布最新人事调整(5张数据表揭开公司真实情况)
  16. 创建新用户时的相关缺省设置
  17. 解决:Android4.3锁屏界面Emergency calls only - China Unicom与EMERGENCY CALL语义重复
  18. Java基础篇--概念理解(泛型、注解)
  19. 自定义View将圆角矩形绘制在Canvas上
  20. 单位根检验、协整检验和格兰杰因果关系检验三者之间的关系

热门文章

  1. 计算机网络技术学什么?
  2. ZuulFilter统一异常处理
  3. java 切面 注解_Java自学之spring:使用注解进行面向切面编程(AOP)
  4. laravel5中model命名与数据库命名解说
  5. 工行闪酷卡 网银圈存
  6. 如何使用PHP书写汉字九九乘法表
  7. 腾讯文学之路,任重而道远
  8. 手机续航测试软件哪个好,6部热捧手机续航测试:iPhoneX倒数第三第一不得不服...
  9. Ubuntu20 修改系统时区为 国内时间
  10. 什么是su和exit命令?