I cooked up this little game today for the community challenge mentioned here. The idea of the game is that sometimes when Simon gives you a color, you should choose the opposite color (the color in the opposite corner). The game visually indicates this when it is an Opposite Simon. I'm not totally sure that the gameplay is "fun", but it was definitely fun to make!
First up, here is the game model:
SOGame.h
#import <Foundation/Foundation.h>
#import "SOGameStates.h"
#import "SOSimonSays.h"
@interface SOGame : NSObject
@property GameState state;
@property NSMutableArray *sequence;
-(void) update;
-(void) evaluateMove:(SimonSays)playerChoice;
-(void) restart;
@end
SOGame.m
#import "SOGame.h"
#import "SOSimon.h"
@implementation SOGame {
int _selectionNumber;
}
-(id) init {
self = [super init];
if (self) {
_state = GameStateStarted;
_sequence = [[NSMutableArray alloc]init];
_selectionNumber = 0;
}
return self;
}
#pragma mark - Game Rules
-(void) update {
switch (self.state) {
case GameStateStarted:
[self createFirstSequence];
break;
case GameStateSuccess:
[self createNewSequence];
break;
default:
break;
}
}
-(void) evaluateMove:(SimonSays)playerChoice {
//if an invalid node is selected
if (playerChoice == SimonSaysNumChoices) {
return;
}
SOSimon *simon = [_sequence objectAtIndex:_selectionNumber];
//different rules for opposite versus normal simons
if (simon.opposite) {
if (playerChoice == [self opposititePosition:simon.simonSays]) {
[self doSuccessfulChoice];
} else {
_state = GameStateFailure;
}
} else {
if (playerChoice == simon.simonSays) {
[self doSuccessfulChoice];
} else {
_state = GameStateFailure;
}
}
}
-(void) doSuccessfulChoice {
if (_selectionNumber < _sequence.count - 1) {
_selectionNumber++;
} else {
_state = GameStateSuccess;
_selectionNumber = 0;
}
}
-(NSInteger) opposititePosition:(SimonSays)position {
switch (position) {
case SimonSaysTopLeft:
return SimonSaysBottomRight;
case SimonSaysTopRight:
return SimonSaysBottomLeft;
case SimonSaysBottomLeft:
return SimonSaysTopRight;
case SimonSaysBottomRight:
return SimonSaysTopLeft;
default:
return 0;
}
}
-(void) restart {
_selectionNumber = 0;
_sequence = nil;
_state = GameStateStarted;
}
#pragma mark - Sequences
-(void) createFirstSequence {
self.sequence = [self sequence:nil];
self.state = GameStateSequenceReady;
}
-(void) createNewSequence {
self.sequence = [self sequence:self.sequence];
self.state = GameStateSequenceReady;
}
-(NSMutableArray *) sequence:(NSMutableArray *)oldSequence {
NSMutableArray *newSequence = [[NSMutableArray alloc]init];
if (oldSequence) {
newSequence = [[NSMutableArray alloc]initWithArray:oldSequence];
}
[newSequence addObject:[[SOSimon alloc]init]];
return newSequence;
}
@end
SOSimon.h
#import <Foundation/Foundation.h>
#import "SOSimonSays.h"
@interface SOSimon : NSObject
@property SimonSays simonSays;
@property BOOL opposite;
@end
SOSimon.m
#import "SOSimon.h"
static const int chanceForOpposite = 5;
@implementation SOSimon
-(instancetype) init {
self = [super init];
if (self) {
_simonSays = arc4random_uniform(SimonSaysNumChoices);
_opposite = NO;
if (arc4random_uniform(100) < chanceForOpposite) {
_opposite = YES;
}
}
return self;
}
@end
SOSimonSays.h
typedef NS_ENUM(NSInteger, SimonSays){
SimonSaysTopLeft = 0,
SimonSaysTopRight,
SimonSaysBottomLeft,
SimonSaysBottomRight,
SimonSaysNumChoices
};
SOGameStates.h
typedef NS_ENUM(NSInteger, GameState){
GameStateStarted = 0,
GameStateSequenceReady,
GameStateSequencePlaying,
GameStateWaitingForInput,
GameStateSuccess,
GameStateFailure,
GameStateEnded
};
Now we have the SKScene. This is fully working at the moment, including animations when the sequence is played and when the player clicks one of the corners. The biggest challenge was to get the actions to play in sequence rather than all playing at the same time. To achieve this, I made a special class called SOQueuedAction
:
SOQueuedAction.h
#import <Foundation/Foundation.h>
#import <SpriteKit/SpriteKit.h>
#import "SOSimon.h"
@interface SOQueuedAction : NSObject
-(id) initWithAction:(SKAction *)action node:(SKSpriteNode *)sprite simon:(SOSimon *)simon;
@property SKAction *action;
@property SKSpriteNode *sprite;
@property SOSimon *simon;
@end
SOQueuedAction.m
#import "SOQueuedAction.h"
@implementation SOQueuedAction
-(id) initWithAction:(SKAction *)action node:(SKSpriteNode *)sprite simon:(SOSimon *)simon {
self = [super init];
if (self) {
_action = action;
_sprite = sprite;
_simon = simon;
}
return self;
}
@end
This basically just stores the action to perform and the sprite to perform it on, and then the game scene handles them one at a time:
SOGameScene.m
#import "SOGame.h"
#import "SOGameScene.h"
#import "SOQueuedAction.h"
#import "SOSimon.h"
#import "SOSimonSays.h"
NSString* const kNameTopLeft = @"topLeft";
NSString* const kNameTopRight = @"topRight";
NSString* const kNameBottomLeft = @"bottomLeft";
NSString* const kNameBottomRight = @"bottomRight";
@implementation SOGameScene {
CGSize _initialScreenSize;
SOGame *_game;
SKNode *_gameBoard;
SKNode *_overlayNode;
SKLabelNode *_turnLabel;
NSMutableArray *_actionQueue;
BOOL _isActionPlaying;
}
#pragma mark - Initialization
-(void) didMoveToView:(SKView *)view {
_actionQueue = [[NSMutableArray alloc]init];
_initialScreenSize = self.frame.size;
self.backgroundColor = [SKColor whiteColor];
_game = [[SOGame alloc]init];
[self setUpGameBoard];
[self setUpLabels];
_overlayNode = [[SKNode alloc]init];
[self addChild:_overlayNode];
}
-(void) setUpGameBoard {
_gameBoard = [[SKNode alloc]init];
[self addChild:_gameBoard];
float widthDivisor = 3.05;
float heightDivisor = 2.59;
CGPoint topLeftPos = CGPointMake(_initialScreenSize.width/widthDivisor,
_initialScreenSize.height - _initialScreenSize.height/heightDivisor);
CGPoint topRightPos = CGPointMake(_initialScreenSize.width - _initialScreenSize.width/widthDivisor,
_initialScreenSize.height - _initialScreenSize.height/heightDivisor);
CGPoint bottomLeftPos = CGPointMake(_initialScreenSize.width/widthDivisor,
_initialScreenSize.height/heightDivisor);
CGPoint bottomRightPos = CGPointMake(_initialScreenSize.width - _initialScreenSize.width/widthDivisor,
_initialScreenSize.height/heightDivisor);
SKSpriteNode *pieceTopLeft = [self gamePieceForColor:[SKColor greenColor] position:SimonSaysTopLeft];
pieceTopLeft.position = topLeftPos;
[_gameBoard addChild:pieceTopLeft];
SKSpriteNode *pieceTopRight = [self gamePieceForColor:[SKColor yellowColor] position:SimonSaysTopRight];
pieceTopRight.position = topRightPos;
[_gameBoard addChild:pieceTopRight];
SKSpriteNode *pieceBottomLeft = [self gamePieceForColor:[SKColor redColor] position:SimonSaysBottomLeft];
pieceBottomLeft.position = bottomLeftPos;
[_gameBoard addChild:pieceBottomLeft];
SKSpriteNode *pieceBottomRight = [self gamePieceForColor:[SKColor blueColor] position:SimonSaysBottomRight];
pieceBottomRight.position = bottomRightPos;
[_gameBoard addChild:pieceBottomRight];
}
-(NSString *) nameForPosition:(SimonSays)position {
switch (position) {
case SimonSaysTopLeft:
return kNameTopLeft;
case SimonSaysTopRight:
return kNameTopRight;
case SimonSaysBottomLeft:
return kNameBottomLeft;
case SimonSaysBottomRight:
return kNameBottomRight;
default:
return @"Invalid Position";
}
}
-(void) setUpLabels {
SKLabelNode *titleLabelFirstLine = [self titleLabel:@"Simon's"];
titleLabelFirstLine.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height - _initialScreenSize.height/10);
[self addChild:titleLabelFirstLine];
SKLabelNode *titleLabelSecondLine = [self titleLabel:@"Opposites"];
titleLabelSecondLine.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height - _initialScreenSize.height/6);
[self addChild:titleLabelSecondLine];
SKLabelNode *turnWordLabel = [self defaultSizeLabel:@"Turn"];
turnWordLabel.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/6);
[self addChild:turnWordLabel];
_turnLabel = [self defaultSizeLabel:[NSString stringWithFormat:@"%lu", (unsigned long)_game.sequence.count]];
_turnLabel.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/10);
[self addChild:_turnLabel];
}
#pragma mark - Game Loop
-(void)update:(CFTimeInterval)currentTime {
[_game update];
if (_game.state == GameStateSequenceReady) {
[self createActionSequence];
} else if (_game.state == GameStateSequencePlaying) {
[self playActionSequence];
} else if (_game.state == GameStateFailure) {
SKLabelNode *message = [self titleLabel:@"Failed!"];
message.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
[_overlayNode addChild:message];
SKSpriteNode *retryButton = [self retryButton];
retryButton.position = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/3);
[_overlayNode addChild:retryButton];
}
_turnLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)_game.sequence.count];
}
#pragma mark - Touch Controls
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
for (UITouch *touch in touches) {
if(touch.tapCount == 1) {
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
}
}
-(void)selectNodeForTouch:(CGPoint)touchLocation {
if (_game.state == GameStateWaitingForInput) {
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
[_game evaluateMove:[self simonSaysForName:touchedNode.name]];
[self doQuickFadeAnimation:touchedNode];
} else {
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
if ([touchedNode.name isEqualToString:@"retryButton"]) {
[_game restart];
[_overlayNode removeAllChildren];
}
}
}
-(SimonSays) simonSaysForName:(NSString *)name {
if ([name isEqualToString:kNameTopLeft]) {
return SimonSaysTopLeft;
} else if ([name isEqualToString:kNameTopRight]) {
return SimonSaysTopRight;
} else if ([name isEqualToString:kNameBottomLeft]) {
return SimonSaysBottomLeft;
} else if ([name isEqualToString:kNameBottomRight]) {
return SimonSaysBottomRight;
}
return SimonSaysNumChoices;
}
#pragma mark - Animations
-(void) createActionSequence {
for (SOSimon *simon in _game.sequence) {
SKSpriteNode *nodeToAnimate = nil;
switch (simon.simonSays) {
case SimonSaysTopLeft:
nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameTopLeft];
break;
case SimonSaysTopRight:
nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameTopRight];
break;
case SimonSaysBottomLeft:
nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameBottomLeft];
break;
case SimonSaysBottomRight:
nodeToAnimate = (SKSpriteNode *)[_gameBoard childNodeWithName:kNameBottomRight];
break;
default:
break;
}
if (nodeToAnimate) {
SKAction *fadeOutAndIn = [SKAction sequence:@[[SKAction colorizeWithColor:[SKColor whiteColor]
colorBlendFactor:0.5 duration:1],
[SKAction colorizeWithColor:nodeToAnimate.color
colorBlendFactor:0.5 duration:1]
]];
SOQueuedAction *actionForQueue = [[SOQueuedAction alloc]initWithAction:fadeOutAndIn
node:nodeToAnimate
simon:simon];
[_actionQueue addObject:actionForQueue];
}
}
_game.state = GameStateSequencePlaying;
}
-(void) playActionSequence {
if (!_isActionPlaying) {
if (_actionQueue.count > 0) {
_isActionPlaying = YES;
SOQueuedAction *action = [_actionQueue firstObject];
if (action.simon.opposite) {
[action.sprite addChild:[[SKSpriteNode alloc]initWithColor:[SKColor blackColor] size:CGSizeMake(action.sprite.size.width*0.85, action.sprite.size.height*0.85)]];
}
[action.sprite runAction:action.action completion:^{
[action.sprite removeAllChildren];
_isActionPlaying = NO;
if (_actionQueue.count == 0) {
_game.state = GameStateWaitingForInput;
[self playActionSequence];
}
}];
[_actionQueue removeObjectAtIndex:0];
}
}
}
-(void) doQuickFadeAnimation:(SKSpriteNode *)sprite {
SKColor *originalColor = sprite.color;
[sprite runAction:[SKAction colorizeWithColor:[SKColor whiteColor]
colorBlendFactor:0.5 duration:0.2] completion:^ {
[sprite runAction:[SKAction colorizeWithColor:originalColor
colorBlendFactor:0.5 duration:0.2]];
}];
}
#pragma mark - UI Elements
-(SKLabelNode *) titleLabel:(NSString *)text {
SKLabelNode *titleLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
titleLabel.text = text;
titleLabel.fontSize = 30;
titleLabel.fontColor = [SKColor blackColor];
return titleLabel;
}
-(SKLabelNode *) defaultSizeLabel:(NSString *)text {
SKLabelNode *defaultSizeLabel = [[SKLabelNode alloc]initWithFontNamed:@"Arial"];
defaultSizeLabel.text = text;
defaultSizeLabel.fontColor = [SKColor blackColor];
return defaultSizeLabel;
}
-(SKSpriteNode *) gamePieceForColor:(SKColor *)color position:(SimonSays)position {
SKSpriteNode *gamePiece = [[SKSpriteNode alloc]initWithColor:color size:CGSizeMake(_initialScreenSize.width/3, _initialScreenSize.width/3)];
gamePiece.name = [self nameForPosition:position];
return gamePiece;
}
-(SKSpriteNode *) retryButton {
SKSpriteNode *retryButton = [[SKSpriteNode alloc]initWithColor:[SKColor purpleColor] size:CGSizeMake(_initialScreenSize.width/2, _initialScreenSize.height/5)];
retryButton.name = @"retryButton";
SKLabelNode *retryLabel = [self defaultSizeLabel:@"Retry?"];
retryLabel.name = @"retryButton";
[retryButton addChild:retryLabel];
return retryButton;
}
@end
I think that my code is pretty clean overall, but I am always open to any kind of criticism. If there are any huge problems, they are probably in the GameScene class.
Here is a screenshot of the game: