I have a view that creates SCNView dynamically. It's scene is empty, but when I press a button I would like to add a node from separate scn file. This file contains animation, and I would like it to animate in main scene. The problem is that after adding object to the scene it's not animating. When I use this file as SCNView scene it works. isPlaying and loops are enabled. What else do I need to do to import such node with animation? Sample code below:
override func viewDidLoad() {
super.viewDidLoad()
let scene = SCNScene()
let sceneView = SCNView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
sceneView.scene = scene
sceneView.loops = true
sceneView.isPlaying = true
sceneView.autoenablesDefaultLighting = true
view.addSubview(sceneView)
let subNodeScene = SCNScene(named: "Serah_Animated.scn")!
let serah = subNodeScene.rootNode.childNode(withName: "main", recursively: false)!
scene.rootNode.addChildNode(serah)
}
You need to fetch the animation from your scene Serah_Animated.scn, which will be a CAAnimation object. You then add that animation object to the rootNode of your main scene.
let animScene = SCNSceneSource(url:<<URL to your scene file", options:<<Scene Loading Options>>)
let animation:CAAnimation = animScene.entryWithIdentifier(<<animID>>, withClass:CAAnimation.self)
You can find the animID from the .scn file using scene editor in Xcode, as shown below.
Now you can add the animation object to your root node.
scene.rootNode.addAnimation(animation, forKey:<<animID>>)
Note that we are reusing animID, that will allow you to also remove the animation from the node.
scene.rootNode.removeAnimation(forKey:<<animId>>)
My solution above assumes your animation is a single animation. If you see a bunch of animations, you need to add all the animation nodes. In my workflow, I have files in Blender which I export to Collada format and then use the Automated Collada Converter to ensure I have single animation nodes.
Related SO answer
You can also fetch the animID programmatically using entriesWithIdentifiersOfClass(CAAnimation.self), useful when you have a bunch of animations instead of a single animation as above or if you just want to add the animation without bothering about the animID before hand.
Apple Sample Code for scene kit animations, note the sample code is in ObjC but translation to Swift should be straight forward.
All you need is retrieving animations:
[childNode enumerateChildNodesUsingBlock:^(SCNNode *child, BOOL *stop) {
for(NSString *key in child.animationKeys) { // for every animation key
CAAnimation *animation = [child animationForKey:key]; // get the animation
animation.usesSceneTimeBase = NO; // make it system time based
animation.repeatCount = FLT_MAX; // make it repeat forever
[child addAnimation:animation forKey:key]; // animations are copied upon addition, so we have to replace the previous animation
}
}];
Related
So I'm hoping to learn about making a scrollview of icons (kinda like a menu) in Spritekit, using Swift. I can't find any good resources or tutorials that don't charge.
I have my basic app set up with a ViewController and my scene.
I'm hoping to have a scrollview that's say 2 or 3 times longer than the height of the screen, where I can scroll up and down and view different icons.
I hope this would be a good implementation, so I can programmatically set all the icons using x/y coordinates.
My viewdidLoad:
override func didMoveToView(view: SKView) {
/* Setup your scene here */
addChild(worldNode)
//create Scrollview
//for loop to adding icons from top to bottom of scrollview
//name sprites
//use touch location so allow sprites to be clickable
//will just adjust alpha of icon for testing purposes later
}
This is really all I'm looking at achieving at the moment, if you can imagine, something like this:
Some of the questions that I have seen here seem to go in far more detail than I imagine I need, and are too complex for me at this stage, or they are not in swift...
How can I include a scrollview?
Thank you in advance :)
If your entire UI is a scene built with SpriteKit, you may want to consider building it as if it were a scrolling scene with a camera. Here's an example in a gameplay context
Basically it could work like this:
var sceneCam: SKCameraNode! //declare your camera variable in your scene class
Then, in your didMoveToView function:
sceneCam = SKCameraNode() //initialize your camera
//scaleAsPoint lets you zoom the camera in and out
sceneCam.scaleAsPoint = CGPoint(x: 0.25, y: 0.25)
camera = sceneCam //set the scene's camera
addChild(sceneCam) //add camera to scene
//position the camera on the scene.
sceneCam.position = CGPoint(x: frame.center.x, y: frame.center.y)
Now, in your touchesMoved gesture handler, when you detect the pan, you can adjust the camera position to match the pan gesture translation.
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
let touch = touches.anyObject() as UITouch
let positionInScene = touch.locationInNode(self)
let previousPosition = touch.previousLocationInNode(self)
let translation = CGPoint(x: positionInScene.x - previousPosition.x, y: positionInScene.y - previousPosition.y)
sceneCam.position = CGPoint(x: sceneCam.position.x - translation.x, y: sceneCam.position.y - translation.y)
}
The other crucial part of the setup is just to make your scene the right size for your menu. This is a similar concept to your content size on a scrollview. You might also have some work to do to implement paging or similar UIScrollView features if you desire those. But if you just need a simple scrolling view, you could do it with a camera.
If you ONLY want UIKit elements on this view, it should probably just be it's own VC without a scene. You could even create this menu view controller and place it in a container on your game scene - that way you'd be using only UIKit components in your menu vc, but you can still use it in a SpriteKit setting.
Main menu looks like this.
When i press start code below changes the scene.
let newScene = LevelScene(size: self.scene!.size)
let transition = SKTransition.revealWithDirection(SKTransitionDirection.Up, duration: 1)
newScene.scaleMode = SKSceneScaleMode.AspectFill
self.scene!.view!.presentScene(newScene, transition: nil)
And after I return to the main menu it looks like this
or this
So, as you can see, sprites just disappears. I can't come up with a reason for this to happen.
Are you doing,
self.addChild(...)
self.addChild(...)
in your didMoveToView method?
Another possible problem might be that you are setting the zPosition of the node to less than your background, and / or other nodes.
I had the same problem with sprites disappearing and after changing the zPositions of all my sprites and background I still wasn't able to solve it.
I ended up just redeclaring the scene inside my restartButton:
var scene = SKScene(fileNamed: "GameScene")
scene?.scaleMode = .aspectFill
view!.presentScene(scene)
And that worked for me.
I am loading a third-party .dae Collada file as a scene into a SceneKit project.
The .dae file has many different animations in it, set at different times/frames. I am trying to figure out how I can split these out and reference each individual animation by reference name. There are no intelligible reference names in the dae file - the animations are all set as one single animation.
I am able to parse out the animations into a CAAnimation object, and verify that I have successfully done so with the following code:
SCNScene *scene = [SCNScene sceneNamed:#"art.scnassets/man.dae"];
SCNNode *man = [scene.rootNode childNodeWithName:#"Bip01" recursively:YES];
CAAnimation *animation = [man animationForKey:#"test_Collada_DAE-1"];
[man removeAllAnimations];
[man addAnimation:animation forKey:#"animation"];
Is there any way to set a start and end frame or time to my CAAnimation object? What is the best way to parse out the various animations? I'm hoping that I do not have to manually split the dae file into many and load each one individually.
3d tools often export multiple animations as a single animation with sub animations. In that case SceneKit will load these animations as a CAAnimationGroup with sub animations. So one option is to "parse" the animation group's sub animations and retrieve the ones you want. Another option is to retrieve (sub)animations by name using SCNSceneSource (but this will work only if your 3d tool exported names when it exported your DAE).
If you need to "crop" animation (i.e extract an animation that starts at t0 with duration D from a longer animation), CoreAnimation has a APIs for that:
create an animation Group to "crop" with duration D.
add the animation you want to crop as a sub-animation and set it's
timeOffset to t0.
As Toyos mentioned in his answer, enumerate the CAAnimationGroup using SCNSceneSource and retrieve the CAAnimation objects as follows:
NSURL *daeURL = [[NSBundle mainBundle] URLForResource:#"exportedFilename" withExtension:#"dae"];
SCNSceneSource *sceneSource = [SCNSceneSource sceneSourceWithURL:daeURL options:nil];
NSMutableArray *myAnimations = [#[] mutableCopy];
for (NSString *singleAnimationName in [sceneSource identifiersOfEntriesWithClass:[CAAnimation class]]) {
CAAnimation *thisAnimation = [sceneSource entryWithIdentifier:singleAnimationName withClass:[CAAnimation class]];
[myAnimations addObject:thisAnimation];
}
Here is code that converts frame numbers to times and then plays only that portion of the animation by using a CAAnimationGroup as #Toyos described. This sample code plays an "idle" animation by repeating frames 10 through 160 of fullAnimation:
func playIdleAnimation() {
let animation = subAnimation(of:fullAnimation, startFrame: 10, endFrame: 160)
animation.repeatCount = .greatestFiniteMagnitude
addAnimation(animation, forKey: "animation")
}
func subAnimation(of fullAnimation:CAAnimation, startFrame:Int, endFrame:Int) -> CAAnimation {
let (startTime, duration) = timeRange(startFrame:startFrame, endFrame:endFrame)
let animation = subAnimation(of: fullAnimation, offset: startTime, duration: duration)
return animation
}
func subAnimation(of fullAnimation:CAAnimation, offset timeOffset:CFTimeInterval, duration:CFTimeInterval) -> CAAnimation {
fullAnimation.timeOffset = timeOffset
let container = CAAnimationGroup()
container.animations = [fullAnimation]
container.duration = duration
return container
}
func timeRange(startFrame:Int, endFrame:Int) -> (startTime:CFTimeInterval, duration:CFTimeInterval) {
let startTime = timeOf(frame:startFrame)
let endTime = timeOf(frame:endFrame)
let duration = endTime - startTime
return (startTime, duration)
}
func timeOf(frame:Int) -> CFTimeInterval {
return CFTimeInterval(frame) / framesPerSecond()
}
func framesPerSecond() -> CFTimeInterval {
// number of frames per second the model was designed with
return 30.0
}
I'm using standard full screen SpriteKit view from the boilerplate code (new game iOS project), and when I use
scene.scaleMode = .AspectFit
to make sure that scene fits, remaining (letterboxed) area is colored in black.
I've tried these options to change the color:
let skView = self.view as SKView
// was hoping this one would work
skView.backgroundColor = UIColor.redColor()
// didn't expect this would work, since scene is scaled anyways
scene.backgroundColor = SKColor.redColor()
Also tried to change SKView background color in storyboard editor to another color, but no luck.
Any tips on where to look to change the color of the letterboxed area?
I think the should be a way to set the colour or place sprites in the ears of the letterhead... but it seems that there isn't. Also, I disagree with LearnCocos2D. There's nothing wrong with AspectFit. It has its use.
What I've done is use AspectFit but also calculate the size of the scene based on screen size. This isn't that helpful for an OSX game but it'll work fine for iOS.
func createScene() -> GameScene {
let screenSize = UIScreen.mainScreen().bounds.size
var size:CGSize = CGSizeZero
size.height = Constants.sceneHeight
size.width = screenSize.width * (size.height / screenSize.height);
return GameScene(size: size)
}
override func viewDidLoad() {
super.viewDidLoad()
let scene = self.createScene()
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = false
skView.showsNodeCount = false
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFit
skView.presentScene(scene)
}
After a scene is rendered, its contents are copied into the presenting view. If the view and the scene are the same size, then the content can be directly copied into the view. If the two differ, then the scene is scaled to fit in the view. The scaleMode property determines how the content is scaled.
The letterboxing is create IN the view so there is no way around it. That said there is often the need for an .aspectFit setting if you want to use a specific scene size and universal coordinate system. If that is your case, the following should help.
You need to figure out how much a standard .aspectFit would need in padding to remove the letter boxing from your current device and add that to your scene. You finish with a scene that is lighter a little bigger or a little wider then your original sizes but the scene has at least the same size as your original scene size. You can then move the anchorPoint to the original (0, 0) coordinates or the (0.5, 0.5) one depending on what coordinate system you want to use for your game.
The code for this is convoluted and could change in time. If you want a quick fix, I created a Framework exactly for that purpose. It's small but portable :)
https://github.com/Tokuriku/tokuriku-framework-stash
Just:
Download the ZIP file for the Repository
Open the "SceneSizer" sub-folder
Drag the SceneSizer.framework "lego block" in your project
Make sure that the Framework in Embedded and not just Linked
Import the Framework somewhere in your code import SceneSizer
And you're done, you can now call the sizer Class with:
SceneSizer.calculateSceneSize(#initialSize: CGSize, desiredWidth: CGFloat, desiredHeight: CGFloat) -> CGSize
Documentation is in the folder for a clean and full use with a standard scene. Hope this helps!
I need to add a rain particle effect to my app, I have been having a tough time finding ways to actually execute this idea.
I tried following this CALayer approach tutorial : Link but I am not quite sure if this is the best approach, considering the new iOS 7 SpriteKit Particle Emitter available in Xcode 5.
I have already created the .sks file and it's in my Hierarchy, but I am still unable to add it to my storyboard / project.
With that being said, How exactly do I add a SpriteKit Particle (sks) to my view? I am not at all familiar with scenes, layering , etc in the SpriteKit framework as I am not a game developer.
I need the most details and sample code possible so that I can figure this out please
UPDATE:
I have followed the direction provided in an answer by fellow SO member: AyatollahAndy, please see his answer below. Although I was able to display the SKScene in my view the app crashes when any touch event is received. I get the following:
Thanks
Create a SKScene in your UIView to add a SKEmitterNode particle effect.
One way of doing this:
1.In storyboard (or programatically if you prefer) add a View object on top of the existing View and resize it to your needs.
2.Change the class of the new view to SKView
3.In your view controller .h file create a property for the SKView:
#property IBOutlet SKView *skView;
4.Link the SKView on your storyboard to the skView property.
5.Create a new class, subclassing SKScene. MyScene.h will look like:
#import <SpriteKit/SpriteKit.h>
#interface MyScene : SKScene
#end
MyScene.m below contains code to create a particle effect whenever and wherever the SKView is touched.
#import "MyScene.h"
#implementation MyScene
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
/* Setup your scene here */
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
SKLabelNode *myLabel = [SKLabelNode labelNodeWithFontNamed:#"Chalkduster"];
myLabel.text = #"Hello, World!";
myLabel.fontSize = 30;
myLabel.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
[self addChild:myLabel];
}
return self;
}
//particle explosion - uses MyParticle.sks
- (SKEmitterNode *) newExplosion: (float)posX : (float) posy
{
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"MyParticle" ofType:#"sks"]];
emitter.position = CGPointMake(posX,posy);
emitter.name = #"explosion";
emitter.targetNode = self.scene;
emitter.numParticlesToEmit = 1000;
emitter.zPosition=2.0;
return emitter;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
for (UITouch *touch in touches) {
CGPoint location = [touch locationInNode:self];
//add effect at touch location
[self addChild:[self newExplosion:location.x : location.y]];
}
}
-(void)update:(CFTimeInterval)currentTime {
/* Called before each frame is rendered */
}
#end
6.In your main view controller, include your scene class:
#import "MyScene.h"
and add code to viewDidLoad to initialise the SKView:
- (void)viewDidLoad
{
[super viewDidLoad];
// Configure the SKView
SKView * skView = _skView;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
// Present the scene.
[skView presentScene:scene];
}
You should then have a working SKScene within your main UIView.
In modern Xcode:
This is now very easy.
1. In Xcode, click to create a new
"SpriteKit Particle File"
it will be a single .sks file.
(NOTE: Do NOT choose "SceneKit Particle System File". Choose "SpriteKit Particle File".)
Click once on the .sks file. Notice the many controls on the right.
The particles will actually be moving, it is a living preview. Anything that can be done with particles, you can do it. It is like using particles in a game engine, except performance is 18 billion times better.
2. Have any ordinary UIView, anywhere you want:
#IBOutlet weak var teste: UIView! // totally ordinary UIView
3. Just use the following code to link:
The following slab of code will put your new particle system, inside, the ordinary UIView "teste":
import SpriteKit ...
let sk: SKView = SKView()
sk.frame = teste.bounds
sk.backgroundColor = .clear
teste.addSubview(sk)
let scene: SKScene = SKScene(size: teste.bounds.size)
scene.scaleMode = .aspectFit
scene.backgroundColor = .clear
let en = SKEmitterNode(fileNamed: "SimpleSpark.sks")
en?.position = sk.center
scene.addChild(en!)
sk.presentScene(scene)
Add this to anything you want.
If you want a sparkling button, add it to a button.
If you want the whole screen to shower rainbows, add it to a full-screen view.
It's that easy.
Example of how to use the SpriteKit Particle File controls:
Say you want a burst of sparks, which ends.
Set the max to 50...
Tip - if your effect "finishes" (ie, it is not a loop), it seems you can simply get rid of the SKScene when finished. Like this:
...
scene.addChild(en!)
sk.presentScene(scene)
delay(1.5) { sk.removeFromSuperview() }
That one line of code at the end seems to clean-up everything.
BTW if you want fantastic ideas for particle systems, a great idea is click to the Unity "asset store", where various particle artists buy and sell particle systems. Their work will give you great ideas.
Just click "particles" in the list on the right; watch the videos. (Innovative examples .)
Note! Apple are going to make it so that you can very simply make a SKView in storyboard, and select the .sks scene. However ..
... it does not work yet! It's still broken as of the last edit to this post (2020). So you need the code fragment above.
You can add SKView as a subview within your UIKit hierarchy. A function like the following would work, allowing you to create a UIImageView with the effect as a subview, and then you can add this to your main view. Be sure to link against SpriteKit.
UIImageView *NewEffectUIImageViewWithFrame(CGRect frame)
{
UIImageView *tempView = [[UIImageView alloc] initWithFrame:frame];
SKView *skView = [[SKView alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
[tempView addSubview:skView];
SKScene *skScene = [SKScene sceneWithSize:skView.frame.size];
skScene.scaleMode = SKSceneScaleModeAspectFill;
skScene.backgroundColor = [UIColor clearColor];
SKEmitterNode *emitter = [NSKeyedUnarchiver unarchiveObjectWithFile:[[NSBundle mainBundle] pathForResource:#"SparkParticle" ofType:#"sks"]];
emitter.position = CGPointMake(frame.size.width*0.5,0.0);
[skScene addChild:emitter];
[skView presentScene:skScene];
return tempView;
}
In the end, if all you need is an emitter, it may be easier to create a CAEmitterLayer and add that as a sublayer to your UIView instead. Of course, that means you have to programmatically create the CAEmitterLayer and can't use the cool Xcode particle editor...
Here's approach totally different approach to try. My buddy gave me this cool way to go. Using CAEmitterCell. All in code! Looks like you need a spark.png image.
extension UIView {
final public func ignite() {
let emitter = CAEmitterLayer()
emitter.frame = self.bounds
emitter.renderMode = kCAEmitterLayerAdditive
emitter.emitterPosition = self.center
self.layer.addSublayer(emitter)
let cell = CAEmitterCell()
let bundle = Bundle.init(for: UIColor.self)
let image = UIImage(named: "spark", in: bundle, compatibleWith: traitCollection)
cell.contents = image?.cgImage
cell.birthRate = 1500
cell.lifetime = 5.0
cell.color = UIColor(red: 1.0, green: 0.5, blue: 0.1, alpha: 1).cgColor
cell.alphaSpeed = -0.4
cell.velocity = 50
cell.velocityRange = 250
cell.emissionRange = CGFloat.pi * 2.0
emitter.emitterCells = [cell]
}
}
Enjoy.
Actually there is a way to add particles without SpriteKit - CoreAnimation's CAEmitterCells.
This way you can add particles in your UIView easily. If you want to play around with the parameters and get the code easily, get this app (Particle X).
It also supports SpriteKit so if you want to play around or design particles on the go and immediately get the code for it, this app is the solution.
PS. If you haven't noticed it, I am the developer of the app - made it to use it myself when designing app and games. :)
Putting this here for visibility reasons.
The answers regarding the user of a .clear backgroundColor are correct, except that you must also set the allowsTransparency property on SKView to 'true'.
skView.allowsTransparency = true
skView.backgroundColor = .clear // (not nil)
scene.backgroundColor = .clear
If you don't set allowsTransparency to true, and you layout your SKView over, say, a UIImageView, the composition engine will have a fit, and will send your GPU red-lining, even if only a single particle is drawn. (In the Simulator, the CPU will spike instead.)
You cannot use particle effects within UIView directly.
SKEmitterNode must be in a node tree defined with a node scene (SKScene). The scene node runs an animation loop that renders the contents of the node tree for display. UIView is static, won't work for it.
However, you probably able to create a scene inside your UIView, but I've never tried to do that.