How to have textureFromNode produce texture for retina display? - ios

Platform: XCode 6.0 + iOS 7.0.4
Running the following code on iPad mini retina and expect the image of tmpLbl and node to render identically because the latter is initialized with the texture of the former, via textureFromNode. In reality tmpLbl, which is an SKLabelNode, renders at retina resolution but node, which is supposed to be a copy, is blurry, appearing to be a double-scaled rendition of a half-sized texture.
I saw a post suggesting to put the source (tmpLbl in the sample below) on the scene but it makes no difference, as illustrated by the sample.
Any advice on:
1. how to make this work using any sort of approach
2. how to make this work without having the source node (tmpLbl below) ever rendered on the screen
Sample code:
- (void)didMoveToView:(SKView *)view {
SKLabelNode *tmpLbl = [SKLabelNode labelNodeWithFontNamed: #"Futura"];
tmpLbl.position = CGPointMake(100, 100);
[self addChild: tmpLbl];
tmpLbl.fontSize = 125;
tmpLbl.fontColor = [SKColor colorWithRed:0.8 green:0.0 blue:0.0 alpha:1.0];
tmpLbl.text = #"a";
SKTexture *texture = [view textureFromNode: tmpLbl];
SKNode *node = [SKSpriteNode spriteNodeWithTexture:texture];
node.position = CGPointMake(200, 130);
[self addChild:node];
return;
}
Rendering result; the left side "a" is via SKLabelNode, the right side one is via textureFromNode and spriteNodeWithTexture:

Solution to the problem: it is necessary but not sufficient for the source node-tree passed to textureFromNode to be part of the scene as suggested by others. Another condition for the returned texture to be of "retina resolution" is that textureFromNode: is called not earlier (per the results of my experiments) than the first call to SKScene update:. (Update: on XCode 5.0.1 textureFromNode: must be called in during or after the second call to update:; textureFromNode during the first call still produces non-retina texture.)
Just by moving the code in the question above to the update: function the resulting texture will have all the pixels for retina rendering. What is frustrating is that the texture will be double the size of the source node (tree) in both directions and initializing a new sprite with the returned sprite will produce a double-size sprite (that is rendered with interpolation). It is clear that the SKTexture class has an internal 'scale' property, just like UIImage. SpriteKit sets this property when creating the texture from a file with the '#2x' suffix but not when the texture is created via textureFromNode. This is very unfortunate because this forces the application developer to track the scale manually unless the texture is created from an image resource.
The updated sample below includes the following changes compared to the original in the question:
move textureFromNode: (along with the rest of the code) to update:; anyone has a better place that would still produce a retina-size rendition?
detect if the display resolution is retina and resize the new sprite to the size of the source-sprite if needed
With these changes the two 'a' images are rendered identically.
// retina resolution will be not applied if textureFromNode is called before SKScene::update():
-(void)update:(CFTimeInterval)currentTime {
static int cnt = 0;
if (cnt == 1) // can be cnt == 0 on XCode 6.0
{
double scale=1.0;
if ([[UIScreen mainScreen] respondsToSelector:#selector(displayLinkWithTarget:selector:)] && ([UIScreen mainScreen].scale == 2.0)) {
// the texture returned by textureFromNode is double-size on retina displays; must explicitly make the sprite half the size of the texture:
scale=0.5;
}
SKLabelNode *tmpLbl = [SKLabelNode labelNodeWithFontNamed: #"Futura"];
tmpLbl.position = CGPointMake(100, 100);
// must be part of the scene or else retina resolution will be not applied:
[self addChild: tmpLbl];
tmpLbl.fontSize = 125;
tmpLbl.fontColor = [SKColor colorWithRed:0.8 green:0.0 blue:0.0 alpha:1.0];
tmpLbl.text = #"a";
SKTexture *texture = [self.view textureFromNode: tmpLbl];
SKNode *node = [SKSpriteNode spriteNodeWithTexture:texture size: CGSizeMake(texture.size.width*scale, texture.size.height*scale)];
node.position = CGPointMake(200, 130);
[self addChild:node];
// was added to the scene only to force retina resolution rendering:
// [tmpLbl removeFromParent];
}
++cnt;
}
Rendering result with the updated code. The right-hand-side image is not blurry anymore:

Thanks for the idea to perform textureFromNode after update: method is called.
As for a better place that would still produce a retina-size rendition, I find it clearer to call put rasterization code in dispatch_once block to avoid unnecessary counter increment.
In my GameScene (Swift):
var rasterizationToken: dispatch_once_t = 0
override func didFinishUpdate() {
dispatch_once(&rasterizationToken) {
self.rasterizeLayers() //calls textureFromNode inside
}
}
didFinishUpdate() is called after update(), therefore retina resolution remains.

Your label should be added to scene, otherwise it will have non-retina resolution in rendered texture.
Maybe it's duplicate question
Sprite Kit - Low Resolution Texture on Retina when using textureFromNode

I develop a work around for SKLabelNode in Swift 3:
extension SKLabelNode {
func pixelated() {
let v = SKView()
self.fontSize *= 4
let texture = v.texture(from: self)
let textureSize = CGSize(width: (texture?.size().width)!/4, height: (texture?.size().height)!/4)
let node = SKSpriteNode(texture: texture, color: .clear, size: textureSize)
node.position = self.position
node.anchorPoint = CGPoint(x: 0.5, y: 0.0)
self.parent?.addChild(node)
self.removeFromParent()
}
}

Related

Change Physical body frame in spritekit

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

Maintain Sprite position relative to frame/screen when auto-scaling from iphone 5 to 6?

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;

Sprite Kit: Anchor Point is Changing Sprite`s actual Position

please help me sort this confusion out.
From Sprite Kit Programming Guide:
A sprite node’s anchorPoint property determines which point in the
frame is positioned at the sprite’s position.
My understanding of this is that if I change the Anchor Point, the sprite`s position should stay unchanged and only the texture rendering should be moved accordingly.
But when I set the anchor point, my sprite`s position actually changes! Take a look at this snippet:
/* debug */
if (self.currentState == self.editState) {
printf("B: relativeAnchorPoint = %.02f,%.02f ", relativeAnchorPoint.x, relativeAnchorPoint.y);
printf("position = %.02f,%.02f\n",self.position.x, self.position.y);
}
[self setAnchorPoint:relativeAnchorPoint];
/* debug */
if (self.currentState == self.editState) {
printf("A: relativeAnchorPoint = %.02f,%.02f ", relativeAnchorPoint.x, relativeAnchorPoint.y);
printf("position = %.02f,%.02f\n",self.position.x, self.position.y);
}
Output:
A: relativeAnchorPoint = 0.65,0.48 position = 1532.00,384.00
B: relativeAnchorPoint = 0.65,0.48 position = 1583.00,384.00
What am I missing?
Thanks in advance
*edit: additional info: *
it only happens when my sprite has xScale to -1 to invert image
I made a quick test to confirm your observation, and it is indeed correct.
As the xScale becomes negative the anchorPoint does actually affect the node's position.
I tend to think of this as a bug since there seems to be no correlation between the negative xScale and the increase in x position. And it can't be considered normal behavior.
Also this only happens when you change the anchorPoint after the xScale is already negative. You can set anchorPoint, then change xScale all you want and things will be fine, position will not change.
I confirmed this issue exists in both Xcode 5.1 (iOS 7) and Xcode 6 beta (iOS 8 beta).
If you run the following code in a newly created Sprite Kit project in place of its auto-created MyScene.m file you'll see that as anchorPoint changes randomly between 0.0 and 1.0 the position of the sprite always remains the same until the xScale property changes to a negative value. At that point position.x starts to increase significantly.
#import "MyScene.h"
#implementation MyScene
{
SKSpriteNode *sprite;
}
-(id) initWithSize:(CGSize)size
{
if (self = [super initWithSize:size])
{
self.backgroundColor = [SKColor colorWithRed:0 green:0 blue:0.2 alpha:1];
sprite = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
sprite.position = CGPointMake(CGRectGetMidX(self.frame),
CGRectGetMidY(self.frame));
sprite.anchorPoint = CGPointMake(0.2, 0.7);
[self addChild:sprite];
SKAction *action = [SKAction scaleXTo:-1.0 duration:10];
[sprite runAction:[SKAction repeatActionForever:action]];
}
return self;
}
-(void) update:(CFTimeInterval)currentTime
{
sprite.anchorPoint = CGPointMake(arc4random_uniform(10000) / 10000.0,
arc4random_uniform(10000) / 10000.0);
NSLog(#"pos: {%.1f, %.1f}, xScale: %.3f, anchor: {%.2f, %.2f}",
sprite.position.x, sprite.position.y, sprite.xScale,
sprite.anchorPoint.x, sprite.anchorPoint.y);
}
#end
There is a workaround for this bug:
If xScale is already negative, invert it, then set the anchorPoint, then re-invert xScale. You may need to do the same with yScale if that too can become negative.
The following update method incorporates this workaround and I confirmed that this is working as intended:
-(void) update:(CFTimeInterval)currentTime
{
BOOL didInvert = NO;
if (sprite.xScale < 0.0)
{
didInvert = YES;
sprite.xScale *= -1.0;
}
sprite.anchorPoint = CGPointMake(arc4random_uniform(10000) / 10000.0,
arc4random_uniform(10000) / 10000.0);
if (didInvert)
{
sprite.xScale *= -1.0;
}
NSLog(#"pos: {%.1f, %.1f}, xScale: %.3f, anchor: {%.2f, %.2f}",
sprite.position.x, sprite.position.y, sprite.xScale,
sprite.anchorPoint.x, sprite.anchorPoint.y);
}
The sprite.position now remains the same throughout the entire scaleXTo action duration.

Tilemap floor NOT display before background picture

I am having a little trouble doing something that is supposed to be very simple.
I cannot get the floor of my tiles to display above a background picture. However I can get all my other game objects to show from my control pad, to my HUD to even coins and monsters set up in the same tile map. Basically everything appears in front the background like I expect the floor of my tilemap so it looks like im walking on air. I have tried many things like changing which layer i add the background picture or the tilemap floor too, or even tried setting it the same way i set my characters but same results. Tilemap floor is always at the back.
Adding my Set up code, Hope it is helpful too solving the problem.
I created this BG sprite since, I wanted my tilemap to scroll vertically or horzi. automatically. So the easiest way I found to do that was to make the tilemap the child of the "bg" and scroll the "bg" hence scrolling the tile map. However, I have tried setting the background as the child of the bg and setting the Z for both of them but that didnt seem to help.
Thanks in advance for any help in solving this
#implementation GameLevelScene
{
SKNode *_worldNode;
SKSpriteNode *bg;
SKSpriteNode *bkg;
}
Init
-(id)initWithSize:(CGSize)size level:(int)level {
if (self = [super initWithSize:size]) {
// [self showBackground];
NSDictionary *levelData = config[#"levels"][level];
//[show background];
if (levelData[#"tmxFile"]) {
[self showBackground];
_tileMap = [ JSTileMap mapNamed:levelData[#"tmxFile"]];
}
//self.backgroundColor = [SKColor colorWithRed:.4 green:.4 blue:.95 alpha:1.0];
// UIImage *bkgb =[UIImage imageNamed:#"land.jpg"];
// self.position=CGPointZero;
// self.anchorPoint=CGPointZero;
// self.backgroundColor=[UIColor colorWithPatternImage:bkgb];
//Above code shows no picture but it changes the color
[self setUpWorld];
[self createChar];
[self controlPadNode];
//[show background];
}
return self;
}
setUpWorld
- (void)setUpWorld
{
bg = [SKSpriteNode spriteNodeWithImageNamed:#"bg3"];
bg.userInteractionEnabled=NO;
bg.anchorPoint = CGPointZero;
bg.zPosition=0;
bg.position = CGPointZero;
bg.name = #"bg";
[self addChild:bg];
_worldNode = [SKNode node];
if (_tileMap) {
[bg addChild:_tileMap];
}
[bg addChild:_worldNode];
self.physicsWorld.contactDelegate = self;
}
create char
- (void)createChar
{
_Layer = [[TmxTileMapLayer alloc]
initWithTmxObjectGroup:[_tileMap
groupNamed:#"LevelOneObjects"]
tileSize:_tileMap.tileSize
gridSize:_bgLayer.gridSize];
[self addChild:_Layer];
}
Create Control
- (SKSpriteNode *)controlPadNode
//-(void)controlPad
{
SKSpriteNode *controlPadNode = [SKSpriteNode spriteNodeWithImageNamed:#"controller.png"];
controlPadNode.position = CGPointMake(100,50);
controlPadNode.name = #"controlPadNode";
controlPadNode.zPosition = 1.0;
[self addChild:controlPadNode];
}
background
-(void)showBackground
{
bkg = [SKSpriteNode spriteNodeWithImageNamed:#"desert_land.jpg"];
bkg.userInteractionEnabled=NO;
bkg.anchorPoint = CGPointZero;
bkg.position = CGPointZero;
bkg.zPosition=-1;
bkg.name = #"bkg";
// [self addChild:bkg];
//[_tileMap addChild:bkg];
// [_worldNode addChild:bkg];
[bg addChild:bkg];
}
Set your bkg.zposition to 1 and set your bg.zPosition to 2.
Also, the whole point of having a tile map is NOT to use a gigantic background picture but to instead use tiles.
** Update **
I just tested your code and ran a sample project myself. I assume you are using the Tiled app for your tiles. I ran variations on parents (self, worldNode, etc...) and zPositions. The bottom line is you cannot do it. Tiled does not have an alpha channel for its background color options. So either the background image is covered by the tile map or, in your case, the background image covers the tile map.
Your possible solutions are to either not use Tiled and place your tiles manually or to look for another tile app which has an alpha channel.
I noticed that where you define the wall properties of the tile if you say
SKSpriteNode* wall =
[SKSpriteNode spriteNodeWithColor:[SKColor greenColor]
size:CGSizeMake(w, h)];
the green color will be uptop of the background. Bit of a dirty fix than an answer

(Spritekit) iPad resolution not 1024x768?

I'm trying to set a background texture and I want it to cover the entire screen.
I prepared the background files exactly to size in photoshop before hand. There are 2 files in my project:
background.png - 1024x768px
background#2x.png - 2048x1536px
I am running the following code:
SKTexture *backgroundTexture = [SKTexture textureWithImageNamed:#"background"];
SKSpriteNode *background = [SKSpriteNode spriteNodeWithTexture:backgroundTexture];
background.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
background.size = CGSizeMake(750, 550);
[self addChild:background];
And it is giving me this result http://d.pr/i/Ej2m - notice that the entire screen is almost filled and the background size is background.size = CGSizeMake(750, 550). Why is this?
You manually changed the sprite's size:
background.size = CGSizeMake(750, 550);
So it will display in a smaller region (750x550) than its original size (1024x768). Allow me to say: d'uh! ;)

Resources