Making Note Cards that can be Shuffled - RITAccess/accessmath GitHub Wiki

Making the note cards movable and clickable

  • Creating the outline for the cards is simple; just create an SKSpriteNode with a lightly larger frame and make it the parent of the child the note card (of whatever image/color). I came up with this way when I could not find a simpler way to create an outline for the note card I was viewing, and I did not want them to look like they were merging into one form every time they came into contact with one another.

      SKSpriteNode *pap = [self paperNode];
      pap.position = CGPointMake(CGRectGetMidX(outline1.frame), CGRectGetMidY(outline1.frame));
      outline1 = [self outlineNode];
      [outline1 addChild:pap]; 
    
  • In order for them to move as one, a texture should be made of the combined objects. Creating an SKTexture is especially helpful for when the user needs to change the background color of the node. http://stackoverflow.com/questions/19063796/confusion-skspritenode-sktexture-difference

      SKTexture *tex = [self.scene.view textureFromNode:outline1];
      newNode = [SKSpriteNode spriteNodeWithTexture:tex];
      newNode.name = @"newNode";
    

This can then be added as a child of “self”: [self addChild:newNode];

  • Now that a texture for the node has been created, generating the change is very simple:

      SKTexture *tex = [self.scene.view textureFromNode:checkNode];
      SKSpriteNode *paper = [[SKSpriteNode alloc] initWithTexture:tex];
      paper.size = CGSizeMake(300, 200);
      
      SKSpriteNode *outliner = [self outlineNode];
      [outliner addChild:paper];
      
      SKTexture *newTex = [self.scene.view textureFromNode:outliner];
      
      SKAction* changeFace = [SKAction setTexture:newTex];
      
      for(SKNode *check in self.children){
          if([check.name hasPrefix:@"newNode"]){
      //this is where the actual change takes place, via the runAction ability of the SKSpriteNode
      //being viewed.
              [self runAction:changeFace];
              [check runAction:changeFace];
          }
      }
    
  • Finally, in order to make them both clickable and draggable, we start with the touchesBegan method, and count the number of taps the user makes.

      - (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
          _tappedTwice = NO;
          UITouch *touch = [touches anyObject];
          CGPoint scenePosition = [touch locationInNode:self];
    
          SKNode *checkNode = [self nodeAtPoint:scenePosition];
         /*
         Checks to see if the node being clicked is the SKSpriteNode for the paper itself, and thus 
         checks to see if it is being dragged or being double clicked to transition to another page.
         */
         if(checkNode && ([checkNode.name hasPrefix:@"newNode"])){
              if([touch tapCount] == 2){
              //this sends it to the Notes section of the app.)
              _tappedTwice = YES;
              NotesSection *nn = [[NotesSection alloc] initWithSize:CGSizeMake(1024, 768)];
              SKView *view = (SKView *) self.view;
              SKTransition *doors = [SKTransition doorsOpenVerticalWithDuration: 0.5];
             [view presentScene:nn transition:doors];
         }
         else if([touch tapCount] == 1 && !_tappedTwice){
           _activeDragNode = (SKSpriteNode *)checkNode;
           //The following two lines brings the selected note card to the foremost view.
          [checkNode removeFromParent];
          [self addChild:checkNode];
         }
      }
    

If it is being dragged, touchesMoved is called. The following code allows for the dragging to be a seamless movement controlled by the user.

    -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
        if(_activeDragNode != nil){
           UITouch *touch = [touches anyObject];
           CGPoint scenePosition = [touch locationInNode:self];
           CGPoint lastPos = [touch previousLocationInNode:self];
    
           CGPoint newLoc = CGPointMake(_activeDragNode.position.x + (scenePosition.x - lastPos.x),   
                                        _activeDragNode.position.y + (scenePosition.y - lastPos.y));
    
           _activeDragNode.position = newLoc;
        }

    }

Lastly, in touchesEnded, the only thing needed is this: _activeDragNode = nil;

http://www.boxesofbabies.com/tutorial/detail/6/Dragging-Sprites-Around-In-SpriteKit

SKPhysicsBody for creating offsets on SKSpriteNodes

  • The nodes/note cards won’t “hide” behind one another or disappear and will always be accessible to the user.

  • Expanding the size of the physics body will produce a greater offset

  • Each note card that is created needs to have its physicsBody instantiated in order to keep the offset.

  • Utilizing the physicsBody of the node to detect the “collisions” among any the note cards seemed like the smartest route to take. The only extra addition was to stop the view from falling out of the screen (by turning off its gravity), and by disabling its rotation so that it wouldn’t spin like a wheel every time a “collision” occurred.

      newNode.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(20, 20)];
      newNode.physicsBody.categoryBitMask = outline1Category; (represents itself)
      newNode.physicsBody.contactTestBitMask = outline2Category | outline3Category;
      newNode.physicsBody.collisionBitMask = outline2Category | outline3Category;
      //contactTestBitMask and collisionBitMask defines the categories that create the collision effect
      newNode.physicsBody.affectedByGravity = NO; (won’t fall out of view this way)
      newNode.physicsBody.allowsRotation = NO; (keeps an upright orientation)
    

http://cupial.eu/working-with-collisions-and-contacts-in-sprite-kit/

Save Mechanics for SpriteKit scene

http://www.raywenderlich.com/63235/how-to-save-your-game-data-tutorial-part-1-of-2

  • Initial implementation came from this tutorial
  • Altered the files to suit the needs of the project
  • Added the properties to the interface files to keep track of the background that the user wants, as well as to keep track of the particular positions they are saved at, or if they want it stacked in their initial positions
  • NSCoding is the primary save method
  • I had no concise method to saving any of the user’s preferences when I first began researching. I had attempted to utilize NSUserDefaults at first, but it did not seem to fit as well when using SpriteKit.

Example of encoding and decoding:

    - (instancetype)initWithCoder:(NSCoder *)decoder
    {
        self = [self init];
        if (self) {
           _current = [decoder decodeObjectForKey:currentTexture];
           _date = [decoder decodeObjectForKey:dateColor];
           _pos1 = [decoder decodeCGPointForKey:posi1];
           _pos2 = [decoder decodeCGPointForKey:posi2];
           _pos3 = [decoder decodeCGPointForKey:posi3];
           _statPos = [decoder decodeCGPointForKey:statPos];
           _statPos2 = [decoder decodeCGPointForKey:statPos2];
           _statPos3 = [decoder decodeCGPointForKey:statPos3];
           _isStacked = [decoder decodeBoolForKey:isStacked];
        }
     return self;
    }

    - (void)encodeWithCoder:(NSCoder *)encoder
    {
        [encoder encodeObject:self.current forKey:currentTexture];
        [encoder encodeObject:self.date forKey:dateColor];
        [encoder encodeCGPoint:self.pos1 forKey:posi1];
        [encoder encodeCGPoint:self.pos2 forKey:posi2];
        [encoder encodeCGPoint:self.pos3 forKey:posi3];
        [encoder encodeCGPoint:self.statPos forKey:statPos];
        [encoder encodeCGPoint:self.statPos2 forKey:statPos2];
        [encoder encodeCGPoint:self.statPos3 forKey:statPos3];
        [encoder encodeBool:self.isStacked forKey:isStacked];
     }

Example of using the given properties to store user data/preferences:

     -(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
        UITouch *touch = [touches anyObject];
        CGPoint scenePosition = [touch locationInNode:self];

        SKNode *checkNode = [self nodeAtPoint:scenePosition];

        if(_activeDragNode != nil){
            UITouch *touch = [touches anyObject];
            CGPoint scenePosition = [touch locationInNode:self];
            CGPoint lastPos = [touch previousLocationInNode:self];
    
            CGPoint newLoc = CGPointMake(_activeDragNode.position.x + (scenePosition.x - lastPos.x), 
                                         _activeDragNode.position.y + (scenePosition.y - lastPos.y));
        
            _activeDragNode.position = newLoc;
        
            //saves the current position of the node
            if(checkNode == newNode){
                [saveData sharedData].pos1 = newLoc;
            }
           [[saveData sharedData] save];
        }

    }

In order to load the saved data, the properties can be used in the following manner:

     newNode.position = [saveData sharedData].pos1;
    [[saveData sharedData] save];

Creating a border for SpriteKit scene

http://stackoverflow.com/questions/29084188/preventing-skspritenode-from-going-off-screen

  • Utilizing constraints worked far better than instantiating physics for “self”, as the bodyWithEdgeLoopFromRect: self.frame did nothing when I used it at first.

      [self setPhysicsBody:[SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame]]; 
    

http://stackoverflow.com/questions/22900298/add-boundaries-to-an-skscene

  • Keeps the note cards within the boundaries of the screen, even if it is zoomed, resized, etc.

Example (provided at source as well):

    // get the screensize CGSize scr = self.scene.frame.size; 

    // setup a position constraint 
    SKConstraint *c = [SKConstraint positionX:[SKRange rangeWithLowerLimit:0 upperLimit:scr.width]     
                                    Y:[SKRange rangeWithLowerLimit:0 upperLimit:scr.width]]; 

    gameFish.constraints = @[c]; // can take an array of constraints

Swipe Menu (change background of note cards)

  • When a swipe from the user is detected, a new SKScene - that contains various backgrounds for the note cards for the user to choose from - slides out. The user has the choice to now choose from the images presented, or to swipe to the right to return to the previous screen.

      - (void)leftFlip:(id)sender{
          [self.view setCenter:CGPointMake(512, 384)];
          [self.view removeGestureRecognizer:zoomIn];
          [self.view removeGestureRecognizer:leftSwipe];
          [self.view removeGestureRecognizer:panRecognizer];
          backgroundImages *backy = [[backgroundImages alloc] initWithSize:CGSizeMake(1024, 768)];
          SKView *view = (SKView *) self.view;
          [view presentScene:backy];
       }
    
  • This method works a lot better than having it appear on the same screen with the note cards, as zooming and panning will affect its orientation and size in a negative manner (meaning that it will disappear from the view at certain points, or just look extremely warped). This was the easiest solution.

  • Selecting a background can be done via the following code:

      if(checkNode && ([checkNode.name hasPrefix:@"image"])){
              [self.view removeGestureRecognizer:closeSwipe];
              [self.view removeGestureRecognizer:zoomIn];
      
              SKTexture *tex = [self.scene.view textureFromNode:checkNode];
              SKSpriteNode *paper = [[SKSpriteNode alloc] initWithTexture:tex];
              paper.size = CGSizeMake(300, 200);
      
              SKSpriteNode *outliner = [self outlineNode];
              [outliner addChild:paper];
      
              SKTexture *newTex = [self.scene.view textureFromNode:outliner];
              [saveData sharedData].current = newTex;
      
              MoreShuffle *backy = [[MoreShuffle alloc] initWithSize:CGSizeMake(2000, 1768)];
              SKView *view = (SKView *) self.view;
              [view presentScene:backy];
       }
    

Change text color

  • There are five colors available for the text color for the dates/notes that appears on the note cards: white, light gray, gray, dark gray, and black. They are presented at the bottom left corner of the screen, and will change the color of the text according to the user’s preference.

      else if (checkNode && [checkNode.name hasPrefix:@"color"]){
              for(SKNode *check in self.children){
                  if([check.name hasPrefix:@"newNode"]){
                     for(SKLabelNode *label in check.children){
                         if([checkNode.name isEqualToString:@"color1"]){
                              label.fontColor = [SKColor blackColor];
                              [saveData sharedData].date = label;
                         }
                         if([checkNode.name isEqualToString:@"color2"]){
                              label.fontColor = [SKColor darkGrayColor];
                              [saveData sharedData].date = label;
                         }
                         if([checkNode.name isEqualToString:@"color3"]){
                              label.fontColor = [SKColor grayColor];
                              [saveData sharedData].date = label;
                         }
                         if([checkNode.name isEqualToString:@"color4"]){
                              label.fontColor = [SKColor lightGrayColor];
                              [saveData sharedData].date = label;
                         }
                         if([checkNode.name isEqualToString:@"color5"]){
                              label.fontColor = [SKColor whiteColor];
                              [saveData sharedData].date = label;
                         }
                      }
                  }
              }
          }
        [[saveData sharedData] save];
      }
    

Then by simply setting that to be initialized when the screen is created, the user’s color of choice will be presented:

    //Note: always check to see if it is nil first, as the program will crash if it is.
    if ([saveData sharedData].date != nil) {
            date = [saveData sharedData].date;
    }
    else{
            date = [self dateNode];
    }

Deployment Target (iOS version): 8.3