学习Sprite Kit最好的途径是实战,下面通过一个例子来初探SpriteKit。通过这个例子,你将会学到以下内容:
- 在一个以SpriteKit框架为基础的游戏中使用场景(scenes)。
- 组织节点数并绘制内容。
- 使用动作(actions)为场景内容做动画。
- 为一个场景添加交互。
- 场景之间的切换。
- 场景中的模拟物理环境。
整个项目需要使用Xocde5.0的集成开发环境。使用Single View Application模版创建项目。创建项目的时候使用以下参数:
- Product Name:SpriteWalkthrough
- Class Prefix:Sprite
- Devices:iPad
Sprite Kit框架内容像其他可视内容一样被放置于一个Window视窗里。SptiteKit框架里的内容都是通过SKView类进行渲染,通常都是首先渲染场景,场景是一个SKScene对象。场景也参与响应链,同时还拥有一些专为游戏匹配的其他特性。
因为SpriteKit框架内容是通过一个视图对象渲染出来的,所以你可以将这个视图与其他视图按照视图层级关系排列。例如你可以创建一个按钮控制层放置在SpriteKit视图之上。或者,你还可以通过按钮为sprite添加交互。之后的例子中,你将看到如何为场景添加交互。
使用Sptite Kit来配置视图控制器
在视图控制器的实现文件头部添加下面代码。
#import <SpriteKit/SpriteKit.h>
- (void)viewDidLoad { [super viewDidLoad]; SKView *spriteView = (SKView *) self.view; spriteView.showsDrawCount = YES; spriteView.showsNodeCount = YES; spriteView.showsFPS = YES; }这段代码打开了性能调试信息模块,用来描述场景的渲染情况。帧频(spriteView.showFPS)是最重要的信息。另外两个是描述了为视图中一共有多少个节点显示出来,渲染内容一共绘制了多少次(越少越好)。
接下来,添加第一个场景。
创建Hello场景:
头文件:
#import <SpriteKit/SpriteKit.h> @interface HelloScene : SKScene @end你不需要做任何修改
#import "HelloScene.h"
- (void)viewWillAppear:(BOOL)animated { HelloScene* hello = [[HelloScene alloc] initWithSize:CGSizeMake(768,1024)]; SKView *spriteView = (SKView *) self.view; [spriteView presentScene: hello]; }
程序运行后会显示一个场景,但是是空的,只有性能调试信息模块。
当你制作一款基于SpriteKit框架的游戏时,你可能会根据模块为你的游戏创建不同的场景。例如,为你的主菜单创建一个单独的场景、为你的游戏模块分配一个场景。我们的这个例子也遵循相同的设计,第一个场景显示一个“Hello World”文本。
通常情况下,我们是在场景已经被视图显示出来的时候为场景创建内容。这个例子中,代码应该写在didMoveToView:方法中,说明场景已经被加载到视图中。
在场景中显示Hello World文本
你的实现文件应该如下:
#import "HelloScene.h" @interface HelloScene () @property BOOL contentCreated; @end @implementation HelloScene @end
这个属性并不需要暴露在外部,所以它实现了一个私有访问权限,定义在实现文件中。
- (void)didMoveToView: (SKView *) view { if (!self.contentCreated) { [self createSceneContents]; self.contentCreated = YES; } }
当场景已经被加载进视图的时候调用didMoveToView:方法,但是在这种情况下,内容部分只应该在场景第一次被加载时创建,所以需要使用之前定义的属性(contentCreated)来判断场景内容是否已经被创建过。
- (void)createSceneContents { self.backgroundColor = [SKColor blueColor]; self.scaleMode = SKSceneScaleModeAspectFit; [self addChild: [self newHelloNode]]; }
为场景定义背景颜色,这里的颜色定义使用的是SKColor,它并不是一个类,它是指向UIColor的一个宏。只是为了让代码更统一。
场景的scaleMode属性确定场景如何缩放来适应视图(这个例子中我四个属性都试了,没看出变化来,知道的讲解一下,请赐教)。
- (SKLabelNode *)newHelloNode { SKLabelNode *helloNode = [SKLabelNode labelNodeWithFontNamed:@"Chalkduster"]; helloNode.text = @"Hello, World!"; helloNode.fontSize = 42; helloNode.position = CGPointMake(CGRectGetMidX(self.frame),CGRectGetMidY(self.frame)); return helloNode; }
在SpriteKit框架中,你不能想使用OpenGL ES或Quartz 2D那样直接执行绘制请求。而是通过创建节点对象并添加他们到场景来添加内容。所有绘制必须由SpriteKit内置类来完成,你可以自定义这些类的行为从而产生不同的图形效果。
你可以通过定义一个action对象描述一些动作,让节点运行。当场景渲染的时候,它就会执行这些动作。
当用户触摸屏幕的时候,文本会做一些动画,然后消失。
为文本添加动画:
helloNode.name = @"helloNode";所有节点都有一个描述自身的name属性。以便之后可以通过name属性寻找对应节点。
通常在游戏中,你可以给场景内容所有同一类型的节点一个相同的name属性。例如你将游戏中所有怪兽节点定义一个相同的name属性:“monster”。
在ios中,所有的节点对象都是UIResponder子类。这意味着你可以创建节点类的子类,并为场景中的节点添加交互。
- (void)touchesBegan:(NSSet *) touches withEvent:(UIEvent *)event { SKNode *helloNode = [self childNodeWithName:@"helloNode"]; if (helloNode != nil) { helloNode.name = nil; SKAction *moveUp = [SKAction moveByX: 0 y: 100.0 duration: 0.5]; SKAction *zoom = [SKAction scaleTo: 2.0 duration: 0.25]; SKAction *pause = [SKAction waitForDuration: 0.5]; SKAction *fadeAway = [SKAction fadeOutWithDuration: 0.25]; SKAction *remove = [SKAction removeFromParent]; SKAction *moveSequence = [SKAction sequence:@[moveUp, zoom, pause, fadeAway, remove]]; [helloNode runAction: moveSequence]; } }为了避免节点重复响应这个进程,代码中清除了节点的name属性。然后定义一些action对象执行不同的动作。最后将这些动作合并在一个队列中,按顺序执行。
创建一个宇宙飞船场景
@import "SpaceshipScene.h" @interface SpaceshipScene () @property BOOL contentCreated; @end @implementation SpaceshipScene - (void)didMoveToView:(SKView *)view { if (!self.contentCreated) { [self createSceneContents]; self.contentCreated = YES; } } - (void)createSceneContents { self.backgroundColor = [SKColor blackColor]; self.scaleMode = SKSceneScaleModeAspectFit; } @end
#import "SpaceshipScene.h"
[helloNode runAction: moveSequence completion:^{ SKScene *spaceshipScene = [[SpaceshipScene alloc] initWithSize:self.size]; SKTransition *doors = [SKTransition doorsOpenVerticalWithDuration:0.5]; [self.view presentScene:spaceshipScene transition:doors]; }];
接下来要为场景添加一个宇宙飞船。你要使用多个SKSpriteNode对象创建一个宇宙飞船,并且在它表面有发光。每一个节点都要执行一些动作。
SKSpriteNode是SpriteKit框架中创建内容最常见的类。它们可以绘制有纹理和无纹理的矩形。这个例子中你将使用无纹理的物体。之后你也可以很容易的使用有纹理的物体将无纹理的物体替换而不需要改变它的行为。有时候,你可能需要使用几十甚至上百个节点为你的游戏创建可视化内容。但是,本质上,这些sprite都使用的是相同的方法去创建,正如我们这个例子。
虽然你可以直接将这三个sprite添加到创景当中国,但SpriteKit框架不提倡这么做。闪烁的光是宇宙飞船的一部分!如果飞船移动,光也要跟着移动。解决的办法是将光变成飞船的子视图。光的坐标就相对于它的父节点位置定位在sptite图像的中心。
添加宇宙飞船
SKSpriteNode *spaceship = [self newSpaceship]; spaceship.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)-150); [self addChild:spaceship];
- (SKSpriteNode *)newSpaceship { SKSpriteNode *hull = [[SKSpriteNode alloc] initWithColor:[SKColor grayColor] size:CGSizeMake(64,32)]; SKAction *hover = [SKAction sequence:@[ [SKAction waitForDuration:1.0], [SKAction moveByX:100 y:50.0 duration:1.0], [SKAction waitForDuration:1.0], [SKAction moveByX:-100.0 y:-50 duration:1.0]]]; [hull runAction: [SKAction repeatActionForever:hover]]; return hull; }
这个方法中创建了一个船体,并且引入了一个新的动作类型。重复的执行传入的动作。这个例子中,将无限的执行队列动作。
SKSpriteNode *light1 = [self newLight]; light1.position = CGPointMake(-28.0, 6.0); [hull addChild:light1]; SKSpriteNode *light2 = [self newLight]; light2.position = CGPointMake(28.0, 6.0); [hull addChild:light2];
- (SKSpriteNode *)newLight { SKSpriteNode *light = [[SKSpriteNode alloc] initWithColor:[SKColor yellowColor] size:CGSizeMake(8,8)]; SKAction *blink = [SKAction sequence:@[ [SKAction fadeOutWithDuration:0.25], [SKAction fadeInWithDuration:0.25]]]; SKAction *blinkForever = [SKAction repeatActionForever:blink]; [light runAction: blinkForever]; return light; }
在真实游戏中,你通常需要节点直接带有交互性。为sprite(精灵)添加行为又多种方式,这个例子只展示其中一种。你将为场景添加一些新的节点并使用物理系统模拟它们运动,实现碰撞效果。
SpriteKit框架提供了一个完整的物理模拟环境,你可以为你的接的添加自动化的行为。不需要为节点实现行为(action),模拟物理环境将自动为节点添加这些行为,使它们的移动。当一个节点与其他处于物理系统中的节点接触的时候,会自动检测碰撞。
为宇宙飞船场景添加物理环境
hull.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:hull.size];
由于重力的原因,飞船会沿着屏幕底部垂直落下。并且飞船移动的动作与重力作用同时存在。
hull.physicsBody.dynamic = NO;现在再次运行程序,飞船不再受到重力的影响,像之前一样运动。之后,由于它属于静态物体,所以碰撞也不会影响它的速率。
SKAction *makeRocks = [SKAction sequence: @[ [SKAction performSelector:@selector(addRock) onTarget:self], [SKAction waitForDuration:0.10 withRange:0.15] ]]; [self runAction: [SKAction repeatActionForever:makeRocks]];场景也是一个节点,因此它也可以执行动作。在这里,由自定义的动作在场景中执行一个方法来创建一个岩石。场景会在一个随机的时间间隔中持续的创建新岩石。
static inline CGFloat skRandf() { return rand() / (CGFloat) RAND_MAX; } static inline CGFloat skRand(CGFloat low, CGFloat high) { return skRandf() * (high - low) + low; } - (void)addRock { SKSpriteNode *rock = [[SKSpriteNode alloc] initWithColor:[SKColor brownColor] size:CGSizeMake(8,8)]; rock.position = CGPointMake(skRand(0, self.size.width), self.size.height-50); rock.name = @"rock"; rock.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:rock.size]; rock.physicsBody.usesPreciseCollisionDetection = YES; [self addChild:rock]; }
大量岩石会从屏幕顶端落下。当岩石碰到飞船,会被反弹开。不需要为岩石添加动作,它们会受到物理系统的作用力发生下降和碰撞。
岩石小而移动迅速,代码中指定了精准碰撞检测也确保碰撞的准确性。
如果你运行一段时间,你会发现即使节点的数量保持很低的值,但是帧频开始下降。这是因为代码中只让场景中的可见节点纳入节点数量计算当中。然而,当岩石掉落至屏幕底部之后,它们仍然存在于场景中。这意味着它们仍然存在于物理环境中。最终,大量的节点导致SpriteKit框架运行缓慢。
-(void)didSimulatePhysics { [self enumerateChildNodesWithName:@"rock" usingBlock:^(SKNode *node, BOOL *stop) { if (node.position.y < 0) [node removeFromParent]; }]; }当场景处理每一帧的时候,都会执行动作和模拟物理环境。这时你的游戏也会在这个时候执行一些其他的自定义代码。现在,当应用运行到一个新的帧动画的时,它在物理环境下,将移出屏幕外的岩石移除,所以,当程序运行时,帧频就比较稳定了。
Attribute selectors are a particularly helpful subset of CSS selectors. They allow us to specify an element by one of its HTML attributes, such as a link's title attribute or an image's alt attribute.
AudioManager:用来对音量大小,声音模式(静音,震动,震动加声音等模式)的管理, 还有用它来注册“插入耳机”时的广播接收者(Action: android.intent.action.MEDIA_BUTTON)
源码(没有Android源码的可以看下我之前的博文,有提供下载地址哈~)所在位置:
Android-4.0/frameworks/base/media/java/android/media/AudioManager.java
一. 首先在应用层面上分析下怎么使用这个类:
1.获取AudioManager实例对象
AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
2.AudioManager能实现的一些基本的功能的函数介绍
adjustStreamVolume(int streamType, int direction, int flags)
/**
方法分析:(通过该方法可以控制特定的声音的音量)
用"步长"调整手机声音大小的函数(Adjusts the volume of a particular stream by one step in a direction.)
这个函数只能用于应用程序对Audio属性的设置或者通话(telephony)应用程序
streamType(表示要处理的声音是哪种):
能使用的streamType的值包括:
STREAM_VOICE_CALL(通话)
STREAM_SYSTEM(系统声音)
STREAM_RING(铃声)
STREAM_MUSIC(音乐)
STREAM_ALARM(闹铃)
direction(“方向”:顾名思义是要往上增加音量,往下减少音量,还是维持不变):
能使用的值有:
ADJUST_LOWER(降低)
ADJUST_RAISE(升高)
ADJUST_SAME(维持原来的)[呵~~呵]
flags(可选标志位):
flags One(单个参数) or more flags.(参数1|参数2|参数3..)
如下flag:
AudioManager.FLAG_SHOW_UI(显示出音量调节UI)
AudioManager.FLAG_ALLOW_RINGER_MODES
AudioManager.FLAG_PLAY_SOUND
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE
AudioManager.FLAG_VIBRATE
对应get方法:
getStreamVolume(int streamType)
最大值为7,最小值为0,当为0时,手机自动将模式调整为“震动模式”
*/
adjustVolume(int direction, int flags)
/**
方法分析:
系统智能地判断现在是要处理的是哪个类型的声音("通话音","媒体音"等)
如果你在打电话,这个时候这个函数相应处理的就是"通话音",而如果你在听歌,处理的就是“媒体音”~~平时其实用一些软件也是有这种感觉的哈~~
direction(“方向”:顾名思义是要往上增加音量,往下减少音量,还是维持不变):
能使用的值有:
ADJUST_LOWER(降低)
ADJUST_RAISE(升高)
ADJUST_SAME(维持原来的)[呵~~呵]
flags(可选标志位):
flags One(单个参数) or more flags.(参数1|参数2|参数3..)
如下flag:
AudioManager.FLAG_SHOW_UI(显示出音量调节UI)
AudioManager.FLAG_ALLOW_RINGER_MODES
AudioManager.FLAG_PLAY_SOUND
AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE
AudioManager.FLAG_VIBRATE
*/
int getMode()
/**
方法分析:
得到声音的模式
返回值(int)对应的宏:
MODE_INVALID: 发生异常的时候返回
MODE_NORMAL: 普通模式
MODE_RINGTONE:铃声模式
MODE_IN_CALL: 通话模式
MODE_IN_COMMUNICATION:通话模式
对应set方法:
setMode(int mode)
*/
int getRingerMode()
/**
方法分析:
得到铃声设置的模式
返回值(int)对应的宏:
RINGER_MODE_NORMAL(铃声模式)
RINGER_MODE_SILENT(静音模式)
RINGER_MODE_VIBRATE(静音但是震动)
对应set方法(改变铃声模式):
setRingerMode(int ringerMode)
*/
getStreamMaxVolume(int streamType)
/**
方法分析:
得到手机最大的音量
*/
setStreamMute(int streamType, boolean state)
/**
方法分析:
让streamType指定的对应的声音流做处理
state为true表示让它静音,false表示不让它静音
如:让音乐静音
setStreamMute(AudioManager.STREAM_MUSIC , true);
*/
3.实例Demo代码
//获取实例 AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); //获取/设置系统音量 audioManager.getStreamVolume(AudioManager.STREAM_SYSTEM); audioManager.setStreamVolume(AudioManager.STREAM_SYSTEM, AudioManager.ADJUST_RAISE , AudioManager.FLAG_SHOW_UI); //获取/设置音乐音量 audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, AudioManager.ADJUST_RAISE , AudioManager.FLAG_SHOW_UI); //其他的类似 //获取/设置铃声的模式 int ringMode = audioManager.getRingerMode(); //普通模式 audioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); //静音模式 audioManager.setRingerMode(AudioManager.RINGER_MODE_SILENT); //其他的类似 //设置声音流静音/不静音 //音乐静音 audioManager.setStreamMute(AudioManager.STREAM_MUSIC, true); //铃声不静音 audioManager.setStreamMute(AudioManager.STREAM_RING, false); //其他的类似
二. 分析下这个类里面的部分源代码
由于知识水平有限,我觉得下面的代码自己分析不好,建议看这篇博文:http://blog.csdn.net/qinjuning/article/details/6938436
/* 为Action == “MEDIA_BUTTON”注册广播接收者 用来广播“耳机插入”的事件 eventReceiver一般接受的参数为这样一个ComponentName对象 如: AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE); ComponentName component = new ComponentName(context.getApplicationContext(),MediaControlReceiver.class); audioManager.registerMediaButtonEventReceiver(component); */ public void registerMediaButtonEventReceiver(ComponentName eventReceiver) { if (eventReceiver == null) { return; } if (!eventReceiver.getPackageName().equals(mContext.getPackageName())) { Log.e(TAG, "registerMediaButtonEventReceiver() error: " + "receiver and context package names don't match"); return; } // construct a PendingIntent for the media button and register it Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); // the associated intent will be handled by the component being registered mediaButtonIntent.setComponent(eventReceiver); PendingIntent pi = PendingIntent.getBroadcast(mContext, 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/); registerMediaButtonIntent(pi, eventReceiver); } /** 为MediaButton注册一个Intent 可以发现,这边也是通过aidl的方式进行的调用 IAudioService的aidl:https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/IAudioService.aidl */ public void registerMediaButtonIntent(PendingIntent pi, ComponentName eventReceiver) { if ((pi == null) || (eventReceiver == null)) { Log.e(TAG, "Cannot call registerMediaButtonIntent() with a null parameter"); return; } IAudioService service = getService(); try { // pi != null service.registerMediaButtonIntent(pi, eventReceiver); } catch (RemoteException e) { Log.e(TAG, "Dead object in registerMediaButtonIntent"+e); } } /** * Unregister the receiver of MEDIA_BUTTON intents. * @param eventReceiver identifier of a {@link android.content.BroadcastReceiver} * that was registered with {@link #registerMediaButtonEventReceiver(ComponentName)}. */ public void unregisterMediaButtonEventReceiver(ComponentName eventReceiver) { if (eventReceiver == null) { return; } // construct a PendingIntent for the media button and unregister it Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); // the associated intent will be handled by the component being registered mediaButtonIntent.setComponent(eventReceiver); PendingIntent pi = PendingIntent.getBroadcast(mContext, 0/*requestCode, ignored*/, mediaButtonIntent, 0/*flags*/); unregisterMediaButtonIntent(pi, eventReceiver); } /** * @hide */ public void unregisterMediaButtonIntent(PendingIntent pi, ComponentName eventReceiver) { IAudioService service = getService(); try { service.unregisterMediaButtonIntent(pi, eventReceiver); } catch (RemoteException e) { Log.e(TAG, "Dead object in unregisterMediaButtonIntent"+e); } } /** * Registers the remote control client for providing information to display on the remote * controls. * @param rcClient The remote control client from which remote controls will receive * information to display. * @see RemoteControlClient */ public void registerRemoteControlClient(RemoteControlClient rcClient) { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } IAudioService service = getService(); try { service.registerRemoteControlClient(rcClient.getRcMediaIntent(), /* mediaIntent */ rcClient.getIRemoteControlClient(), /* rcClient */ // used to match media button event receiver and audio focus mContext.getPackageName()); /* packageName */ } catch (RemoteException e) { Log.e(TAG, "Dead object in registerRemoteControlClient"+e); } } /** * Unregisters the remote control client that was providing information to display on the * remote controls. * @param rcClient The remote control client to unregister. * @see #registerRemoteControlClient(RemoteControlClient) */ public void unregisterRemoteControlClient(RemoteControlClient rcClient) { if ((rcClient == null) || (rcClient.getRcMediaIntent() == null)) { return; } IAudioService service = getService(); try { service.unregisterRemoteControlClient(rcClient.getRcMediaIntent(), /* mediaIntent */ rcClient.getIRemoteControlClient()); /* rcClient */ } catch (RemoteException e) { Log.e(TAG, "Dead object in unregisterRemoteControlClient"+e); } } /** * @hide * Registers a remote control display that will be sent information by remote control clients. * @param rcd */ public void registerRemoteControlDisplay(IRemoteControlDisplay rcd) { if (rcd == null) { return; } IAudioService service = getService(); try { service.registerRemoteControlDisplay(rcd); } catch (RemoteException e) { Log.e(TAG, "Dead object in registerRemoteControlDisplay " + e); } } /** * @hide * Unregisters a remote control display that was sent information by remote control clients. * @param rcd */ public void unregisterRemoteControlDisplay(IRemoteControlDisplay rcd) { if (rcd == null) { return; } IAudioService service = getService(); try { service.unregisterRemoteControlDisplay(rcd); } catch (RemoteException e) { Log.e(TAG, "Dead object in unregisterRemoteControlDisplay " + e); } }