Large Seamless Scrolling Background in SpriteKit? - xcode

Okay, so i'm fairly new to SpriteKit, but so far it's been a lot of fun working with it. I'm looking for a way to have a large scrolling background loop endlessly. I've divided my background into small chunks to let SK call as it needs them, using this code:
for (int nodeCount = 0; nodeCount < 50; nodeCount ++) {
NSString *backgroundImageName = [NSString stringWithFormat:#"Sky_%02d.gif", nodeCount +1];
SKSpriteNode *node = [SKSpriteNode spriteNodeWithImageNamed:backgroundImageName];
node.xScale = 0.5;
node.yScale = 0.5;
node.anchorPoint = CGPointMake(0.0f, 0.5f);
node.position = CGPointMake(nodeCount * node.size.width, self.frame.size.height/2);
node.name = #"skyBG";
and I'm able to move this whole block just fine in update, but I can't get it to loop seamlessly. or at all, for that matter. I've tried the method suggested elsewhere on here that takes a copy of the background and takes it to the other end to give the appearance of a seamless loop, and no dice here. I've also tried adding this to the end of the above code, which I had hoped might work:
if (nodeCount == 49)
{
nodeCount = 0;
}
but that just cause the simulator to hang, so I assume it's inadvertently creating a loop or something. Any assistance at all would be great! I feel like there's a simple fix, but I'm just not getting there... thanks a bunch, folks!
UPDATE
here's the code I'm currently using to get the background to scroll, which works just fine:
-(void)update:(CFTimeInterval)currentTime {
_backgroundNode.position = CGPointMake(_backgroundNode.position.x - 1, _backgroundNode.position.y);
}
So to sum up: I have a moving background, but getting it to loop seamlessly has been challenging because of the size of the background. If there's anything at all I can do to help clarify, let me know! Thanks a bunch

This is just an example of the logic you can employ to achieve a scrolling background. It is by no means a final or ideal implementation, but rather a conceptual example.
Here is a simple scroller class :
// Scroller.h
#import <SpriteKit/SpriteKit.h>
#interface Scroller : SKNode
{
// instance variables
NSMutableArray *tiles; // array for your tiles
CGSize tileSize; // size of your screen tiles
float scrollSpeed; // speed in pixels per second
}
// methods
-(void)addTiles:(NSMutableArray *)newTiles;
-(void)initScrollerWithTileSize:(CGSize)scrollerTileSize scrollerSpeed:(float)scrollerSpeed;
-(void)update:(float)elapsedTime;
#end
Implementation :
// Scroller.m
#import "Scroller.h"
#implementation Scroller
-(instancetype)init
{
if (self = [super init])
{
tiles = [NSMutableArray array];
}
return self;
}
-(void)addTiles:(NSMutableArray *)newTiles
{
[tiles addObjectsFromArray:newTiles];
}
-(void)initScrollerWithTileSize:(CGSize)scrollerTileSize scrollerSpeed:(float)scrollerSpeed
{
// set properties
scrollSpeed = scrollerSpeed;
tileSize = scrollerTileSize;
// set initial locations of tile and set their size
for (int index = 0; index < tiles.count; index++)
{
SKSpriteNode *tile = tiles[index];
//set anchorPoint to bottom left
tile.anchorPoint = CGPointMake(0, 0);
// set tilesize
// this implementation requires uniform size
[tile setSize:tileSize];
//calcuate and set initial position of tile
float startX = index * tileSize.width;
tile.position = CGPointMake(startX, 0);
//add child to the display list
[self addChild:tile];
}
}
-(void)update:(float)elapsedTime
{
//calculate speed for this frame
float curSpeed = scrollSpeed * elapsedTime;
// iterate through your screen tiles
for (int index = 0; index < tiles.count; index++)
{
SKSpriteNode *tile = tiles[index];
// set new x location for tile
tile.position = CGPointMake(tile.position.x - curSpeed, 0);
// if new location is off the screen, move it to the far right
if (tile.position.x < -tileSize.width)
{
// calculate the new x position based on number of tiles an tile width
float newX = (tiles.count -1) * tileSize.width;
// set it's new position
tile.position = CGPointMake(newX, 0);
}
}
}
In your scene , init your scroller :
-(void)initScroller
{
// create an array of all your textures
NSMutableArray *tiles = [NSMutableArray array];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
[tiles addObject:[SKSpriteNode spriteNodeWithTexture:skyTexture]];
// create scroller instance (ivar or property of scene)
scroller = [[Scroller alloc]init];
// add the tiles to the scroller
[scroller addTiles:tiles];
// init the scroller with desired tile size and scroller speed
[scroller initScrollerWithTileSize:CGSizeMake(1024, 768) scrollerSpeed:500];
// add scroller to the scene
[self addChild:scroller];
}
In your scene update method :
-(void)update:(CFTimeInterval)currentTime
{
float elapsedTime = .0166; // set to .033 if you are locking to 30fps
// call the scroller update method
[scroller update:elapsedTime];
}
Again, this implementation is very barebones and is a conceptual example.

I am currently working on a library for a tile scroller, is using a solution similar to the one provided by prototypical, but using a Tableview datasource pattern to ask for the nodes. Take a look at it:
RPTileScroller
I take some effort to make it the most efficient possible, but mind I am not a game developer. I tried it with my iPhone 5 with random colors tiles of 10x10 pixels, and is running on a solid 60 fps.

Related

Processing 3.0 Question - struggling to implement some code

wondering if anyone can help with this. I have to write some ode using processing 3.0 for college, basically creating a series of circles that appear randomly and change colours etc. Ive got the circles to appear, and they change location on mouse click.
What Im struggling with is, its asked me to have the circles change colour when the mouse button is pressed, where circles to the right are blue and circles to the left of the mouse pointer are yellow? I have no idea how to implement that at all.
Here's what I have so far, any help would be hugely appreciated:
//declaring the variables
float[] circleXs = new float[10];
float[] circleYs = new float[10];
float[] circleSizes = new float[10];
color[] circleColors = new color[10];
void setup() {
size(600, 600);
createCircles();
}
//creation of showCricles function
void draw() {
background(0);
showCircles();
}
//creation of circles of random size greater than 10 but less than 50 - also of white background colour
void createCircles() {
for (int i = 0; i < circleXs.length; i++) {
circleXs[i] = random(width);
circleYs[i] = random(height);
circleSizes[i] = random(10, 50);
circleColors[i] = color(255,255,255);
}
}
void showCircles() {
for (int i = 0; i < circleXs.length; i++) {
fill(circleColors[i]);
circle(circleXs[i], circleYs[i], circleSizes[i]);
}
}
//creating new circles on mouse click
void mouseClicked() {
createCircles();
}
It's not very complicated, you just miss some of the basics. Here are 2 things you have to know to do what you want to do:
You can use mouseX or mouseY to compare coordinates with the current mouse pointer's position.
This would be way cleaner using class, but I am guessing that you are not quite there as you're using a couple arrays to store coordinates instead. But here's the thing with that method: the array's index always refer to the same object. Here your objects are circles, so every array's index n refers to the same circle. If you find a circle which x coordinate is leftward compared to the mouse pointer, you can change that circle's color by modifying the item at the same index but in the circleColors array.
So I added a couple lines to your mouseClicked() method which demonstrate what I just said. Here they are:
void mouseClicked() {
createCircles();
// here is the part that I added
// for each circle's X coordinate:
for( int i = 0; i < circleXs.length; i++) {
// if the X coordinate of the mouse is lower than the circle's...
if( mouseX > circleXs[i]) {
// then set it's color to yellow
circleColors[i] = color(255, 255, 0);
} else {
// else set it's color to blue
circleColors[i] = color(0, 0, 255);
}
}
}
It should show what you described, or close enough for you to clear the gap.
Hope it helps. Have fun!

In p5.js, is it possible to load a gif in the setup or preload function, and then use that gif multiple times elsewhere?

I have a class (lets call it "Enemies"), I want them to attack me when close enough (It will display an animated gif, that looks like a bite).
I've gotten all of this to work, except the only way I could figure it out, was by putting loadImage("attack.gif"), in the class. That got laggy really quick, since every time an enemy would spawn, it would have to reload that gif.
I've tried to use a loaded gif from the setup(), in my class, but all of their attacks were in sync.
Is there another way to do this?
You can preload the gif (or gifs) and store them in a array which you can reuse later to draw in multiple places (e.g. multiple spawned enemies in your case).
Here's a basic example that demonstrates:
loading images into an array
spawning objects on screen (re)using the loaded images
let images;
let enemies = [];
function preload(){
// load images and store them in an array
images = [
loadImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAPFBMVEX///8AAADf399fX19/f38/Pz+/v78fHx+fn58XFxeHh4cPDw+np6fX19fHx8evr6+3t7dvb2/39/dPT08jn1NmAAAA+UlEQVR4nO3ZSRKCMBBAUQnzPHj/u7qwOwtS0QhBwfpviaH9bDTG2w0AAAAAgNNLE2HC7zF6T0oAAQT8Q0CTC1M8Df61gywxek8TIcAq5anKXUsIIICACwaMmWhlept5OUvGCAH2o3iLqN8FBBBAwJcDJt3U1Dqsk+1O538/Z0mtU6aPAypnuu4JjfNKwJKKAAIIuEzAi+lxvPuFSQABBJwnoHY2JFu4U4ID7GYqzpYs+KiLAAIIOHPA7D+aWZsPCdh1Wk4AAQRcJ2CRQ5ai1yu9XrnnK/YfqWb9SuqfsoQ/xpoehSf+x1PHHtcTQAABPwsAAAAAAOAYD7+UFrmL/czUAAAAAElFTkSuQmCC"), loadImage("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAclBMVEX///8AAAAMDAwUFBRtbW20tLTKysrr6+s0NDSMjIzo6OgrKytzc3Py8vJKSkolJSWSkpIODg5jY2OlpaW7u7ubm5urq6tEREQaGhr29vZRUVHg4ODDw8Pc3Nw/Pz9VVVWAgIDU1NQ3NzdcXFx6enovLy+wzkUiAAACjElEQVR4nO2a63qCMAxAlU5QEEEUuSMivP8rjptS2iid0qmfOT/XfM0BHGlLZjMEQRAE+ShMXQBTXn79aAgQ6tIE4u1cgG0sTWAvJrBHARRAART4bIFEvUm8Fsk/nxe3p1BH60RskJsIXX+FcnsKkp5GBAQv8nGyEQFHtoCDAiiAAihwP38gXyAAE592LbYhW6B0u1QJnX9nKS2y09d0mTIqv5n+R2KGH0pAW6IACqAACqDAewtYGwaPQGHEY+OsaQRK1WTJoLiMC1PLSQToyI4FULeVBR/3M4nA+nGBeztNFEABFEABcQFgM7WDBHZc2N2NnrgAcVYMWQ7F5Rkb54BF688CkkABFEABFEABWsDcvEBgUGRjm8YdAthtHTbItR2gQp7Z8kQh3mWw4if2ND5M83jPaVoZAIEN0CcCPEciTSCFBPg7IFEAeATAcas8gRz6DfDLJKLKEli+WqAEPgEDbzN5AgYgoPOnEfIELEiA/9YhT8CHBPgTIYkC0TsK+FzYdh8EgWma2lMNdsH5OqHihS1r6E1YhOGxxuurUn4+hhsvTcOxtgUItVg3OH3jRB4FLZrbDfbYwawb1IHy6T4gYPPTeJcL3/Obvv4fg7plV+yJBYDt+TsJHL9eIESBFwtAi/wvE+BXp1MJXFfjQF8fJXD2LYYSOEMcoB449JXCYl3f6JpD2EFqoaxHbLu51hRFU6tp/xIldEGtnho7HzmcFixU00WgsoP3692iKopVVczzZVka1S3xfVJQw8B6Ejj3fAbg49HI9hwFUAAFUOB/BYDDDeUwqUDBCwyO6dzSal7QPcY027oL6pJQk9fJjOEVdjVKjxqSJAE2f0+h13M2XCok3NiIIMgX8guBhVuAILp8rwAAAABJRU5ErkJggg=="),
];
}
function setup(){
createCanvas(600, 600);
imageMode(CENTER);
}
function draw(){
background(255);
// update + draw enemies
for(let i = 0; i < enemies.length; i++){
enemies[i].update();
}
text("click to spawn", 10, 15);
}
function mousePressed(){
// pick a random image
let randomImage = images[int(random(images.length))];
// make a new enemy
enemies.push(new Enemy(randomImage, mouseX, mouseY));
}
class Enemy {
constructor(skin, x, y) {
this.skin = skin;
this.position = createVector(x, y);
this.velocity = createVector(random(-1,1), random(-1,1));
}
update(){
// add velocity
this.position.add(this.velocity);
// check borders and flip direction (roughly)
if(this.position.x < 0 || this.position.x > width ||
this.position.y < 0 || this.position.y > height){
this.velocity.mult(-1);
}
// render
push();
blendMode(MULTIPLY);
image(this.skin, this.position.x, this.position.y);
pop();
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.0/p5.min.js"></script>
In the example above I'm using base64 encoded images to avoid CORS issues, however you should be fine loading your custom images instead.
The idea is to store the images globally so you can re-use them later when instantiating new enemies. (If you have a couple of images individual variables per image should do, otherwise an array will make it easier to manage)
Notice in Enemy constructor simply receives a reference to the previously loaded image:
// when declared
constructor(skin, x, y) {
this.skin = skin;
// when intantiated
new Enemy(randomImage, mouseX, mouseY)
The only thing left to do is to render the image as needed:
image(this.skin, this.position.x, this.position.y);
Update Based on the comment and shared code the is that each enemy shares the same gif which updates at the same rate with the same frame number in sync.
One option would be to fill the images array with multiple copies of the same image, essentially reloading the gif once per enemy, though that seems wasteful.
Unfortunately p5.Image.get() returns a snapshot of the current frame and there is no magic function to clone a loaded gif, however the undocummented gifProperties holds the necessary data to manually do so:
let images;
let enemies = [];
function preload(){
// load images and store them in an array
images = [
loadImage("data:image/gif;base64,R0lGODdhYABgAHcAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAAACwAAAAAYABgAIIAAAAAAACsMjIfHx9NTU0AAAAAAAAAAAAD/wi63P4wykmrvTjrzbv/YCiOZGmeaKqubOu+cCzPdG3feK7vfO//wKBwSCwaj8ikcslsOp/QqHQ6DVivVMsVm6Vsrd2IYPwNjAXhxrl8TjPW33ay7C1/61scfWK/8/02exJ9eX+FOQOJXIOAEVeJAz6Qi46NEI+JkopghpSXVpBBk5wMhJ4AmJFDowEOpqQLqUSsrq+1AaFGrK+muUi7vH2+R8DBZcNMxodVyrBSzc5I0NOtQ9TQRNfNPseZDbwE4afFel/IqK/hBOObyzTdqqXp4tHkRuD01VT469FQ/KeaAOzzb14/OwVNqSMkEJQ3dKYW0JrjMB4vie2UyFJwUTzBxCIbPXwMErLDSCAlOZzMAc+EvUDmHo54WaNlCZpucurcybOnz59AgwodSrSo0aNIkypdyrSp06dPEwAAIfkECQoAAAAsGAADADAASACCAAAAAAAArDIyHx8fTU1NAAAAAAAAAAAAA/8Iutz+MMpJq7046827/2AojmRpnmiqrmzrvnAsz3Rt33iu73xvBsCgLCiMEYGdYkXAPAaYgovzAnVCpUdq83i1BAdgZcQ5nXzBaDGEnDUD0Wmkmy2XnAeLeGBMrzvuDnp8fYNogWF+DX2JDIAQggqLZAuOEpAAkmWYb2AUl5ltmwGGFnqgZKQYpqdHqRerrJVJsT+0JbF7XnW4uJShvLaiSsCsvrmirpGLBMxqAJfGycJszATO0MqjnYrLzYzYFZLVziDi3scj5taMs33jbOXd65Mf6nTtqZJ5iOi62njZ7ikAR6GSvoH8BkXb9kDToQHDfnEC2DDUw4hEFlL8Y7EPwaddE02AkyViZMgSlxIAACH5BAkKAAAALBgAAAAwAEsAggAAAIqKir29vQAAAKwyMh8fH01NTQAAAAP/CLrc/jDKSau9OOvAtYdc8I1MSH6hoJ5eurKZK8BWaHPqTEe3ne+8Hu4FdMiKlSNyolwuTA2oUyF9dqbUa0mLBXEVv24VTJyOAeGues0uDt7w9gIelwPo7zZhjx/sCWp/fX+BfHiEXX19aop4jI11a4t2k3KVbZdsmZKOH5ARkJEOoY2gpKaknQ+pqJsFr5GplXCvBRS1sbJ0c2+1t7B5Crq7wr2vF7jDjb4Zycp9zBjOz3TRI6kG2aI02NrBQN0G2xjUcNnipxPlb+es6uvt6avGtg2Q8cQMzqP0/Ir44/bZ6zfwn7d8CwTe+dZKF4VN/oY9VNVQ1sQBF9dhPKFxN5yGjgwz0DrWDNhGcgSlmfyYEtlKCyPrkVCIytoHmhBissDJQGcRnj6BAG1JQ6HHRBQ1JeU0IAEAIfkECQoAAAAsGAAAADAASACCAAAAvb29ioqKAAAArDIyHx8fTU1NAAAAA/8IutzuIb5Jq43B6k0x/6AHjpdEno+IjkLruiv5zjE7w/WHA66aU7ue6QdsLYQZ4s6BJDKWjaZTAX22fNNKtZrlGaPfrlXADIvP6LR6zW673/B4bkCvx+t2OJ7uJvj3A34Ea4KAgoR/e4dqgIBrjXuPkHlsjnqRl3grkxSTlA6ekJ2ho6GYD6allgwFrZSmq3WtBRqzr7CaCrKtta58uri3A7MftsGQxCPGx4DJIMvMeM6boQbWn0Sm1gbYP9rXvyfRddupFuN05aTn6OqepdPAje7dy6B08QCT9OEL9g27aAGExG+Ag3/6+k2IpmGVKlwNTz2EFdEgO3QKxWHMSGI2I0cOAReYA4DwQ0h5nBSUBImPF0pRKn1Z3HAyxkpULQXalFkxH4mbzVxOgYZMqBOijXzWQJoAACH5BAkKAAAALBgAAwAwAEUAggAAAIqKir29vQAAAKwyMh8fH01NTQAAAAP/CLrc7iG+SauNwepNMf+bIIKkJgplOp1q+2Csm8KjXNKozXn6LPW3H3BILBqPyKRyyWw6n9CodEqtFgfYbDSrhXKxToL4OxATlmayGT3+rpVk8jL+ndO7oLtGbrnr+3WAfnwVhBR/DAWKeAuDAw5ZigUakowKjpBYkpSLYA2Yn5qKH5WOg5skpaZ3qCCqq3GtLbCWOrSeQ7ePKrpZBr+ghb0DvwbBh8PFx5dfssx+yrgLr3TOAI7Ru4mdftbYwNIK1Np7dNm1Nnfn4bbm4FxAw/Ay8ugk9ewfkaPPiKol+yb1ozONG0BRAq8NKljA3oOANv4JslZCorABFFMZDMUlDqOLcWQ8tgDZjN+QcQkAACH5BAkKAAAALBgABgAwAEIAggAAAIqKir29vQAAAKwyMh8fH01NTQAAAAP/CLrcviG+SauNwepNMf+gI4xhWY2CqYrk6r5wLM90bd94ru987//AoHBILBppg6RSqFwGm8kfYQodTAm8a/WapUK5oaqmKraQoeOzs6JeU9pRM1pRqLsnZXayXnDw7w95b3t1fnZxek1pA3wUf4gLcAMOSo0Wj5MNkpSEfZeHmQybmp0fmJJqliCnqGSqpqCtroUvsoAwtpAyuaElvLyLv63BwqOiUK8KkgbMgKyznsdtzAbOsWfJAMvNugDPH9vV3bhw1Le15dy96NPqgirhauTt4mQrlbTacAuYvqXK++iA8scon7F+HPBFW4EwUbYSDQcVXKgiIsAmD188q5LRCMVGZPlmPEsAADs="),
];
}
function setup(){
createCanvas(600, 600);
imageMode(CENTER);
}
function draw(){
background(255);
// update + draw enemies
for(let i = 0; i < enemies.length; i++){
enemies[i].update();
}
text("click to spawn", 10, 15);
}
function mousePressed(){
// pick a random image
let randomImage = images[int(random(images.length))];
// offset the start frame of each enemy by one
let startFrame = enemies.length % randomImage.numFrames();
// make a new enemy
enemies.push(new Enemy(cloneGif(randomImage,startFrame), mouseX, mouseY));
}
function cloneGif(gif, startFrame){
let gifClone = gif.get();
// access original gif properties
gp = gif.gifProperties;
// make a new object for the clone
gifClone.gifProperties = {
displayIndex: gp.displayIndex,
// we still point to the original array of frames
frames: gp.frames,
lastChangeTime: gp.lastChangeTime,
loopCount: gp.loopCount,
loopLimit: gp.loopLimit,
numFrames: gp.numFrames,
playing: gp.playing,
timeDisplayed: gp.timeDisplayed
};
// optional tweak the start frame
gifClone.setFrame(startFrame);
return gifClone;
}
class Enemy {
constructor(skin, x, y) {
this.skin = skin;
this.x = x;
this.y = y;
}
update(){
image(this.skin, this.x, this.y);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.0/p5.min.js"></script>
The cloneGif makes a new object (which means new start frame, etc.), however it should point to the original gif's frames (which hold the pixels / ImageData)
Now each enemy starts at a new frame.
I noticed that changing the delay seems to affect all enemies: once a delay is set to one it seems the underlying imagedata is copied at the same rate.
If you need to have different delay times you can manage still access the same gif frame ImageData, but rather than relying on p5.Image to control the gif playback (e.g. setFrame() / delay()) you'd need manually manage that and render to p5's canvas:
let images;
let enemies = [];
function preload(){
// load images and store them in an array
images = [
loadImage("data:image/gif;base64,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"),
];
}
function setup(){
createCanvas(600, 600);
imageMode(CENTER);
}
function draw(){
background(255);
// update + draw enemies
for(let i = 0; i < enemies.length; i++){
enemies[i].update();
}
text("click to spawn", 10, 15);
}
function mousePressed(){
// pick a random image
let randomImage = images[int(random(images.length))];
// make a new enemy
let enemy = new Enemy({frames: randomImage.gifProperties.frames, gifWidth: randomImage.width, gifH: randomImage.height}, mouseX, mouseY);
// pick a random start frame
enemy.currentFrame = int(random(images.length));
// pick a random per gif frame delay (e.g. the larger the number the slower the gif will play)
enemy.frameDelay = int(random(40, 240));
enemies.push(enemy);
}
class Enemy {
constructor(gifData, x, y) {
this.frames = gifData.frames;
this.offX = -int(gifData.gifWidth * 0.5);
this.offY = -int(gifData.gifHeight * 0.5);
this.currentFrame = 0;
this.numFrames = this.frames.length;
this.frameDelay = 100;
this.lastFrameUpdate = millis();
this.x = x;
this.y = y;
}
update(){
let millisNow = millis();
// increment frame
if(millisNow - this.lastFrameUpdate >= this.frameDelay){
this.currentFrame = (this.currentFrame + 1) % this.numFrames;
this.lastFrameUpdate = millisNow;
}
// render directly to p5's canvas context
drawingContext.putImageData(this.frames[this.currentFrame].image, this.x + this.offX, this.y + this.offY);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.3.0/p5.min.js"></script>
Also notice the p5.Image does something nice about transparency (even when the original gif is missing it): that's something you'd need to manually address if going this lower level route.

UIScrollView does not always animate deceleration when overriding scrollViewWillEndDragging

Here is my override code - it just calculates where to snap to:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if(targetContentOffset->y < 400) {
targetContentOffset->y = 0;
return;
}
int baseCheck = 400;
while(baseCheck <= 10000) {
if(targetContentOffset->y > baseCheck && targetContentOffset->y < baseCheck + 800) {
targetContentOffset->y = (baseCheck + 340);
return;
}
baseCheck += 800;
}
targetContentOffset->y = 0;
}
When I hold my finger down for more than a second or two to drag the scrollview and then lift my finger, it usually animates into place. However, when I do a quick "flick" it rarely animates - it just snaps to the targetContentOffset. I am trying to emulate default paging behavior (except trying to snap to custom positions).
Any ideas?
I ended up having to animate it manually. In the same function, I set the targetContentOffset to where the user left off (current contentOffset) so that it doesnt trigger it's own animation, and then I set the contentoffset to my calculation. Additionally, I added in a velocity check to trigger'automatic' page changes. It's not perfect, but hopefully it will help other with the same issue.
(I modified the above function for readability since no one needs to see my calculations)
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint newOffset = CGPointMake(targetContentOffset->x, targetContentOffset->y);
*targetContentOffset = CGPointMake([broadsheetCollectionView contentOffset].x, [broadsheetCollectionView contentOffset].y);
if(velocity.y > 1.4) {
newOffset.y += pixelAmountThatWillMakeSurePageChanges;
}
if(velocity.y < -1.4) {
newOffset.y -= pixelAmountThatWillMakeSurePageChanges;
}
// calculate newoffset
newOffset.y = calculatedOffset;
[scrollView setContentOffset:newOffset animated:YES];
}

cocos2d :my rotation with the accelerometer is not smooth

so I use the accelerometer in cocos2d to rotate my sprite but the rotation isn't smooth at all . I know that I have to use filter but I don't know how to integrate it in my code :
-(id) init
{
self.isAccelerometerEnabled = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:1/60];
}
- (void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
ombreoeuf1.rotation = acceleration.y * 90 ;
}
sorry for my english I'm french :/
Here's how to implement a lowpass filter. Experiment a bit with kFilteringFactor until you get nice results.
// Declare an int `accelY` in your class interface and set it to 0 in init
-(void) accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration {
float kFilteringFactor = 0.1;
accelY = (acceleration.y * kFilteringFactor) + (accelY * (1.0 - kFilteringFactor));
ombreoeuf1.rotation = accelY * 90;
}
One thing that might help You with the smoothness is to set the update interval to 30 fps instead of 60, so update your init to:
-(id) init
{
self.isAccelerometerEnabled = YES;
[[UIAccelerometer sharedAccelerometer] setUpdateInterval:1.0/30];
}

Jaggy paths when blitting an offscreen CGLayer to the current context

In my current project I need to draw a complex background as a background for a few UITableView cells. Since the code for drawing this background is pretty long and CPU heavy when executed in the cell's drawRect: method, I decided to render it only once to a CGLayer and then blit it to the cell to enhance the overall performance.
The code I'm using to draw the background to a CGLayer:
+ (CGLayerRef)standardCellBackgroundLayer
{
static CGLayerRef standardCellBackgroundLayer;
if(standardCellBackgroundLayer == NULL)
{
CGContextRef viewContext = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].applicationFrame.size.width, PLACES_DEFAULT_CELL_HEIGHT);
standardCellBackgroundLayer = CGLayerCreateWithContext(viewContext, rect.size, NULL);
CGContextRef context = CGLayerGetContext(standardCellBackgroundLayer);
// Setup the paths
CGRect rectForShadowPadding = CGRectInset(rect, (PLACES_DEFAULT_CELL_SHADOW_SIZE / 2) + PLACES_DEFAULT_CELL_SIDE_PADDING, (PLACES_DEFAULT_CELL_SHADOW_SIZE / 2));
CGMutablePathRef path = createPathForRoundedRect(rectForShadowPadding, LIST_ITEM_CORNER_RADIUS);
// Save the graphics context state
CGContextSaveGState(context);
// Draw shadow
CGContextSetShadowWithColor(context, CGSizeMake(0, 0), PLACES_DEFAULT_CELL_SHADOW_SIZE, [Skin shadowColor]);
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context, [Skin whiteColor]);
CGContextFillPath(context);
// Clip for gradient
CGContextAddPath(context, path);
CGContextClip(context);
// Draw gradient on clipped path
CGPoint startPoint = rectForShadowPadding.origin;
CGPoint endPoint = CGPointMake(rectForShadowPadding.origin.x, CGRectGetMaxY(rectForShadowPadding));
CGContextDrawLinearGradient(context, [Skin listGradient], startPoint, endPoint, 0);
// Restore the graphics state and release everything
CGContextRestoreGState(context);
CGPathRelease(path);
}
return standardCellBackgroundLayer;
}
The code to blit the layer to the current context:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawLayerAtPoint(context, CGPointMake(0.0, 0.0), [Skin standardCellBackgroundLayer]);
}
This actually does the trick pretty nice but the only problem I'm having is that the rounded corners (check the static method). Are very jaggy when blitted to the screen. This wasn't the case when the drawing code was at its original position: in the drawRect method.
How do I get back this antialiassing?
For some reason the following methods don't have any impact on the anti-aliassing:
CGContextSetShouldAntialias(context, YES);
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGContextSetAllowsAntialiasing(context, YES);
Thanks in advance!
You can simplify this by just using UIGraphicsBeginImageContextWithOptions and setting the scale to 0.0.
Sorry for awakening an old post, but I came across it so someone else may too. More details can be found in the UIGraphicsBeginImageContextWithOptions documentation:
If you specify a value of 0.0, the scale factor is set to the scale
factor of the device’s main screen.
Basically meaning that if it's a retina display it will create a retina context, that way you can specify 0.0 and treat the coordinates as points.
I'm going to answer my own question since I figured it out some time ago.
You should make a retina aware context. The jaggedness only appeared on a retina device.
To counter this behavior, you should create a retina context with this helper method:
// Begin a graphics context for retina or SD
void RetinaAwareUIGraphicsBeginImageContext(CGSize size)
{
static CGFloat scale = -1.0;
if(scale < 0.0)
{
UIScreen *screen = [UIScreen mainScreen];
if([[[UIDevice currentDevice] systemVersion] floatValue] >= 4.0)
{
scale = [screen scale]; // Retina
}
else
{
scale = 0.0; // SD
}
}
if(scale > 0.0)
{
UIGraphicsBeginImageContextWithOptions(size, NO, scale);
}
else
{
UIGraphicsBeginImageContext(size);
}
}
Then, in your drawing method call the method listed above like so:
+ (CGLayerRef)standardCellBackgroundLayer
{
static CGLayerRef standardCellBackgroundLayer;
if(standardCellBackgroundLayer == NULL)
{
RetinaAwareUIGraphicsBeginImageContext(CGSizeMake(320.0, 480.0));
CGRect rect = CGRectMake(0, 0, [UIScreen mainScreen].applicationFrame.size.width, PLACES_DEFAULT_CELL_HEIGHT);
...

Resources