I have a SpriteKit game which I want to support all orientations. Right now when I change the orientation, the node doesn't keep its position. I use the SKSceneScaleModeResizeFill for scaling, because it will keep the right sprite size.
When I start the game, the game player is positioned in the mid screen like this:
Then when I rotate the device, the position becomes like this:
Here is my view controller code:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
if (!skView.scene) {
// Create and configure the scene.
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeResizeFill;
// Present the scene.
[skView presentScene:scene];
}
}
And my scene code:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0];
//Add spaceship in the center of the view
SKSpriteNode *spaceship = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship.png"];
spaceship.position = CGPointMake(size.width/2, size.height/2);
[spaceship setScale:.3];
[self addChild:spaceship];
}
return self;
}
Your sprite does keep its position after the scene resizes — you can see from your screenshots that it keeps the same horizontal and vertical distance from the lower left corner of the scene. The catch is that after the scene has resized, that absolute offset represents a different relative position in your scene.
Sprite Kit can resize a scene automatically, but the relative positioning of nodes after a scene resize isn't something it can do for you. There's no "right answer" to how a scene's content should be rearranged at a different size, because the arrangement of scene content is something your app defines.
Implement didChangeSize: in your SKScene subclass, and put whatever logic you want there for moving your nodes.
For example, you could make it so nodes keep their positions as a relative proportion of the scene size using something like this:
- (void)didChangeSize:(CGSize)oldSize {
for (SKNode *node in self.children) {
CGPoint newPosition;
newPosition.x = node.position.x / oldSize.width * self.frame.size.width;
newPosition.y = node.position.y / oldSize.height * self.frame.size.height;
node.position = newPosition;
}
}
Depending on what's in your scene and you you've arranged it, you probably don't want that, though. For example, if you have HUD elements in each corner of your game scene, you might want them at a fixed offset from the corners, not a proportion of the scene size.
I add I similar issue and found this question. I solved differently, using the viewWillTransitionToSize:withTransitionCoordinator:, as stated by #GOR here.
I added the following code in my view controller (that manage the SKView and its SKScene)
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
//skView is my SKView object, with scaleMode SKSceneScaleModeResizeFill
skView.frame = CGRectMake(0, 0, size.width, size.height);
//currentScene is my SKScene object
currentScene.size = skView.frame.size;
//Then, as all the objects in my scene are children of a unique SKNode* background, I only relocate it
currentScene.background.position = CGPointMake(CGRectGetMidX(currentScene.frame), CGRectGetMidY(currentScene.frame));
}
and it works like a charm!
For Swift 3,
override func didChangeSize(_ oldSize: CGSize) {
for node in self.children{
let newPosition = CGPoint(x:node.position.x / oldSize.width * self.frame.size.width,y:node.position.y / oldSize.height * self.frame.size.height)
node.position = newPosition
}
}
Thus we are able to use a constant and initialise it in one line. Then in node.position = newPosition we can set the new position.
Also we are able to make use of the enhanced for loop leading to a much more elegant solution.
Related
I develop an iOS game using SpriteKit (such a helpful framework to quickly make a game). I add texture and configure a physical body for a main character as image
The green rectangle is the frame of the physical body. I'm using the following code to create it
#interface MainCharacter : SKSpriteNode
#end
#implementation MainCharacter
+ (instancetype)mainCharacterAtPosition:(CGPoint)pos {
MainCharacter* mainChar = [[MainCharacter alloc] initWithTexture:[SKTexture textureWithImageNamed:#"stand_up"]];
mainChar.position = pos;
mainChar.xScale = 0.5f;
mainChar.yScale = 0.5f;
return mainChar;
}
- (instancetype)initWithTexture:(SKTexture *)texture {
if (self = [super initWithTexture:texture]) {
self.name = kCharacterName;
self.anchorPoint = CGPointMake(0.5f, 0.0f);
[self standup];
CGSize spriteSize = self.size;
CGPoint center = CGPointMake(spriteSize.width*(self.anchorPoint.x-0.5f), spriteSize.height*(0.5f-self.anchorPoint.y));
self.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:spriteSize center:center];
self.physicsBody.dynamic = NO;
self.physicsBody.categoryBitMask = kCharacterCategory;
self.physicsBody.contactTestBitMask = 0x0;
self.physicsBody.collisionBitMask = 0x0;
}
return self;
}
- (void)standup {
SKAction* standupAction = [SKAction setTexture:self.standupTexture resize:YES];
[self runAction:standupAction];
}
- (void)standdown {
SKAction* standownAction = [SKAction setTexture:self.standdownTexture resize:YES];
[self runAction:standownAction completion:^{
}];
[self performSelector:#selector(standup) withObject:nil afterDelay:1.0f];
}
MainCharacter is a class that inherits from SKSPriteNode, just an convienient class to manage a main character. Stand Up is a first state of the character. I have another state, temporarily called stand down (demonstrate as following image)
I add a swipe down gesture to make character stand down.
The green rectangle also the physical body but it's too large for the character. I want to make a physical body frame as the red rectangle.
Can anyone help me how to make the physical body smaller when my character stand down and enlarge the physical body after it stands up
You can destroy the current physics body self.physicsBody = nil; and then simply create a new one with the new size requirements.
I solve this problem by using 2 nodes for 2 states (as a suggestion): stand up state and stand down state. I named it
standupNode and standdownNode
First, add the standupNode to the game scene. If swipe donw gesture recognize, I remove the standupNode from game scene and add the standdownNode instead. On contrary, removing the standdownNode from the game scene then add the standupNode if character stands up
I've developed a game in SpriteKit for a 5s. All my sprites are positioned just where I want them when I run the Xcode simulator on a 4s, 5 or 5s:
http://tinypic.com/r/f0yanl/8
But when I run it on a 6 my sprites are not positioned correctly, and some are even placed off the entire frame/screen:
http://tinypic.com/r/akdac7/8
Notice my HUD is showing up top out of the frame, and the shadow and positioning of my "Machine" at the bottom is off, etc.
Is there a quick, simple fix to ensure the sprites maintain their position relative to my background image, when running on a 6? For now, I'm OK with the entire screen being collapsed a bit when running on a 6 (with empty borders on all four sides), but I would like to make sure everything at least looks just as clean and in order as it does on a 5.
I've used anchor points for most of my nodes, for e.g.:
+ (instancetype) machineAtPosition:(CGPoint)position {
GameMachineNode *machine = [self spriteNodeWithImageNamed:#"machine_1"];
machine.position = position;
machine.name = #"Machine";
machine.anchorPoint = CGPointMake(0.5, 0);
....
}
My "GameViewController.m" includes a resize to fill mode:
SKScene * scene = [GameTitleScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeResizeFill;
Update: figured it out. Was as simple as adding this single line of code to the initWithSize method:
background.size = self.frame.size;
Full code block for reference:
-(id)initWithSize:(CGSize)size {
if (self = [super initWithSize:size]) {
self.lastUpdateTimeInterval = 0; //initializing this property
self.timeSinceEnemyAdded = 0; //initializing this property
self.addEnemyTimeInterval = 1.25;
self.totalGameTime = 0;
self.minSpeed = GameAstroDogMinSpeed;
self.restart = NO;
self.gameOver = NO;
self.gameOverDisplayed = NO;
// Setup your scene here....
SKSpriteNode *background = [SKSpriteNode spriteNodeWithImageNamed:#"background_1"];
background.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
background.size = self.frame.size;
I created a SKView which presents an SKScene subclass like this:
SKView *skv = [[SKView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:skv];
self.skScene = [[TestScene alloc] initWithSize:v.bounds.size];
[skv presentScene:_skScene];
Then to see coordinate system origin, I add a small 10x10 colored square to the scene.
SKSpriteNode *ori = [SKSpriteNode spriteNodeWithColor:[UIColor greenColor] size:CGSizeMake(10,10)];
[self addChild:ori];
You can see green square in lower left corner:
From what I thought, SpriteKit coordinate system is such that origin is always in center. But in the scene the origin is in lower left corner. When I add a child node to {0,0} it appears in lower left corner as well.
When I add a SKSpriteNode and position it {0,0} in the scene, it appears in lower left corner. But it is centered around scene origin in lower left corner (clipped off half to left, and half to bottom).
But now it gets more confusing. When I add a SKSpriteNode to another SKSpriteNode, the sub-sprite is CENTERED in the parent.
So does it mean the Scene coordinate system works different than the sprite coordinate system?
Recap:
When I position a sprite in scene at {0,0}, it appears in bottom
left, clipped off 50% (centered around origin).
When I position a sprite in a sprite at {0,0}, it appears centered in the sprite.
Is my Scene configured wrong or is this the way it works?
This is the way it works. All OpenGL views (the 2D ones at least) have their origin at the lower left corner.
The position of your sprites is correct too. Both are located at 0,0 by default. The texture of the sprites is drawn relative to the node position based on the anchorPoint factor. It's default value of 0.5, 0.5 places the texture centered on the node's position.
You can change the anchorPoint of the scene to 0.5,0.5 which will move the sprites to the center of the scene. You can also change the sprite's anchorPoint though that isn't recommended since it affects things like rotation, scale, collision detection and child node position.
you can add this code to GameViewController.swift :
override func viewDidLoad() {
super.viewDidLoad()
// Detect the screensize
var sizeRect = UIScreen.mainScreen().applicationFrame
var width = sizeRect.size.width * UIScreen.mainScreen().scale
var height = sizeRect.size.height * UIScreen.mainScreen().scale
// Scene should be shown in fullscreen mode
let scene = GameScene(size: CGSizeMake(width, height))
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
What I did was to set the size of the SKScene you want to show to the bounds.size of the Viewcontroller you want to show the scene from. Something like this:
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
scene.size = skView.bounds.size
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
I hope this worked for you as well, if I made any mistakes here, please let me know as well :)
I'm just trying to set up a basic scene in landscape, with gravity, and having the scene in an edge loop.
I set up the scene's physics body and the mainCharacter sprite physics body, here is my code:
-(id)initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
self.backgroundColor = [SKColor redColor];
[self setPhysicsBody:[SKPhysicsBody bodyWithEdgeLoopFromRect:[self frame]]];
}
return self;
}
-(void)setupMain
{
if (!self.mainCharacter)
{
self.mainCharacter = [[SKSpriteNode alloc] initWithImageNamed:#"spriteDefault"];
[self.mainCharacter setPosition:CGPointMake(CGRectGetMidX([self frame]), CGRectGetMidY([self frame]))];
[self addChild:self.mainCharacter];
self.mainCharacter.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.mainCharacter.frame.size];
self.mainCharacter.physicsBody.dynamic = YES;
self.mainCharacter.physicsBody.affectedByGravity = YES;
self.mainCharacter.physicsBody.mass = 0.02;
}
}
So, in portrait mode, everything works perfectly, however, in landscape, things get really screwy.
I figured it has something to do with
[self setPhysicsBody:[SKPhysicsBody bodyWithEdgeLoopFromRect:[self frame]]];
Oddly enough, the edge loop for the x axis for landscape (the y axis in portrait mode) works fine, but I just fall through the y axis (x for portrait).
My guess is that the frame is returning the position on the y axis somewhere not within the bounds of the screen in landscape mode.... meaning its somewhere above or below the screen.
...Maybe... Not really sure.
However, I have tried several different options, including manually setting the rectangle myself by using
CGRectMake()
I wasn't able to get anything to work properly.
Any advice would be greatly appreciated!!!
Okay, this is a super Janky fix...
self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:CGRectMake(0, 200, 320, 200)];
I'm not sure why this works... and it's obviously going to have some problems on smaller screen sizes... better fixes and explanations would be much appreciated!!
Thanks :D
You will need to setup the edge loop in the viewWillLayoutSubviews, since the scene size is only known at then:
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
// Configure the view.
SKView * skView = (SKView *)self.view;
if (!skView.scene) {
skView.showsFPS = YES;
skView.showsNodeCount = YES;
SKScene * scene = [MyScene sceneWithSize:skView.bounds.size];
scene.scaleMode = SKSceneScaleModeAspectFill;
[skView presentScene:scene];
}
}
You need to set the collisionBitMask of your mainCharacter equals to the sceneCategory (need to create). Your nodes will only be affected if you tell them.
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.