主人公の名前を入力

これも昔からお馴染み、「君の名前は?」イベント。
顔見知りなのに名前を知らない不思議。

Confirmと同じくUIAlertViewで済ませます。

まずは設定ファイル。

story_prompt.json

{
    "type" : "story",
    "map" : "map_shop",
    "events" : {
        "c1" : {
            "type" : "message",
            "message" : {
                "en" : "Hi,",
                "ja" : "やあ。"
            },
            "next" : "c1c"
        },
        "c1c" : {
            "type" : "prompt",
            "message" : {
                "en" : "What is your name?",
                "ja" : "君の名前は?"
            },
            "key" : "username",
            "next" : "c1a"
        },
        "c1a" : {
            "type" : "message",
            "message" : {
                "en" : "<username>...OK, Welcome <username>!",
                "ja" : "<username>か、いい名前だ。よろしく、<username>!"
            },
            "next" : "story_prompt"
        }
    }
}

まずは、「やぁ」とメッセージを表示。
その後テキスト入力用のアラートを表示して、入力内容をkeyに指定されているusernameをキーにNSUserDefaultに保存します。
そして、保存された内容を使ってメッセージを表示します。

アラート表示は以下のような実装。

SJStroyScene.m

- (void)processEvent:(NSString *)name {
    
    /* 省略 */

    } else if ([event[@"type"] isEqualToString:@"prompt"]) {
        
        NSString *message = event[@"message"][[SJUtilities lang]];
        
        __weak UIAlertView *alertView = [UIAlertView alertViewWithTitle:nil message:message];
        alertView.alertViewStyle = UIAlertViewStylePlainTextInput;
        [alertView addButtonWithTitle:NSLocalizedString(@"OK", nil) handler:^{
            NSString *text = [alertView textFieldAtIndex:0].text;
            NSString *key = event[@"key"];
            [[NSUserDefaults standardUserDefaults] setObject:text forKey:key];
            [self processEvent:event[@"next"]];
        }];
        [alertView show];
        
    }

}

メッセージ内にある<username>は以下の処理で置換されます。

- (NSString *)replaceKeys:(NSString *)message {
 
    NSMutableString *replaced = message.mutableCopy;
    NSError *error = nil;
    NSRegularExpression *regexp = [NSRegularExpression regularExpressionWithPattern:@"<([^>]+)>" options:0 error:&error];
    if (error) {
        NSLog(@"%@", error.localizedDescription);
    }
    
    NSMutableArray *keys = @[].mutableCopy;
    [regexp enumerateMatchesInString:message options:0 range:NSMakeRange(0, message.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        NSString *key = [message substringWithRange:[result rangeAtIndex:1]];
        [keys addObject:key];
    }];
    
    for (NSString *key in keys) {
        [replaced replaceOccurrencesOfString:[NSString stringWithFormat:@"<%@>", key] withString:[[NSUserDefaults standardUserDefaults] stringForKey:key] options:0 range:NSMakeRange(0, replaced.length)];
    }
    
    return replaced;
}

動作確認してみます。

入力。


黒歴史になりがち

表示。


馴れ馴れしい

できました。

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

コメント

はい・いいえの確認

昔ながらのお使いゲーでお馴染み、はい・いいえの確認。
はいと言うまで前に進ませてくれません。

専用のNodeを作ってもいいのですが、今回は手抜きでUIAlertViewで済ませます。

設定ファイルは以下のとおりです。

story_confirm.json

{
    "type" : "story",
    "map" : "map_shop",
    "events" : {
        "c1" : {
            "type" : "confirm",
            "message" : {
                "en" : "OK?",
                "ja" : "やってくれるな?"
            },
            "yes" : "c1y",
            "no" : "c1n"
        },
        "c1y" : {
            "type" : "message",
            "message" : {
                "en" : "Thank you!",
                "ja" : "おぉ、やってくれるか!では頼んだぞ。"
            },
            "next" : "story_confirm"
        },
        "c1n" : {
            "type" : "message",
            "message" : {
                "en" : "",
                "ja" : "…。"
            },
        }
    }
}

Yesをタップされるとc1yNoをタップされるとc1nに進むようにします。1

SJStroyScene.m

- (void)processEvent:(NSString *)name {
    
    NSDictionary *event = self.sceneData[@"events"][name];

    if (event) {
        [[self playerNode] removeAllActions];
    }

    if ([event[@"type"] isEqualToString:@"message"]) {
        _state = SJStorySceneStateMessage;
        [self messageNode].message = event[@"message"][[SJUtilities lang]];
        [self messageNode].hidden = NO;
        self.nextScene = event[@"next"];
        
    } else if ([event[@"type"] isEqualToString:@"confirm"]) {
        
        NSString *message = event[@"message"][[SJUtilities lang]];
        
        UIAlertView *alertView = [UIAlertView alertViewWithTitle:nil message:message];
        [alertView addButtonWithTitle:NSLocalizedString(@"Yes", nil) handler:^{
            [self processEvent:event[@"yes"]];
        }];
        [alertView addButtonWithTitle:NSLocalizedString(@"No", nil) handler:^{
            [self processEvent:event[@"no"]];
        }];
        [alertView show];
        
    }
    
}

できました。


はいと言うまで許さない

次はテキスト入力を、これもUIAlertViewを使って実装予定。

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

  1. BlocksKitを使用しています。 

コメント

設定シーン

引き続き軽いお題で、設定画面です。
言語の日英切り替えと音のON/OFF、スペシャルサンクスなどを表示します。

コードは単純で、前回作ったSJTapNodeとSKLabelをただ並べているだけです。

1点、コンテンツの高さがシーンより大きくなるので、以下のようにスクロールを実装しています。

- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint positionInScene = [touch locationInNode:self];
    CGPoint previousPosition = [touch previousLocationInNode:self];
    
    CGFloat translationY = positionInScene.y - previousPosition.y;
    
    SKSpriteNode *scrollNode = [self scrollNode];
    CGPoint position = CGPointMake(scrollNode.position.x, scrollNode.position.y + translationY);
    
    CGFloat top = -(scrollNode.size.height - self.frame.size.height);
    CGFloat bottom = 0;
    if (position.y < top) {
        position.y = top;
    } else if (position.y > bottom) {
        position.y = bottom;
    }

    scrollNode.position = position;
}

完成画面はこちら。


TableView風

今回はあえて、Sprite Kitを使っていますが、UIKit(UITableView)を使った方が確実に楽なので、そちらをオススメします。

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

コメント

タイトルシーン

今回は軽いお題で、タイトル画面を作ります。
リリース時には凝った演出があった方がよいかもしれませんが、現状ではタイトルとボタンを並べるだけです。

ボタンはSJTapNodeというクラスで表現しています。
touchesEnded:withEvent:が発生した時にdisabledじゃなければ、targetのactionを呼び出す単純なNodeです。

なお、タップ中やdisabled時はcolorBlendFactorを操作してグレーになるようにしています。

SJTapNode

- (id)initWithFontNamed:(NSString *)fontName {
    if (self = [super initWithFontNamed:fontName]) {
        self.color = [SKColor grayColor];
        self.userInteractionEnabled = YES;
    }
    return self;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if (_disabled) return;
    
    self.colorBlendFactor = BLEND_SELECTED;
}

- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
    if (_disabled) return;

    self.colorBlendFactor = BLEND_NORMAL;
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    if (_disabled) return;
    
    if (_target && [_target respondsToSelector:_action]) {
        [_target performSelector:_action withObject:nil afterDelay:0];
    }

    [self touchesCancelled:touches withEvent:event];
}

- (void)setDisabled:(BOOL)disabled {
    _disabled = disabled;
    self.colorBlendFactor = _disabled ? BLEND_DISABLED : BLEND_NORMAL;
}

これを以下のようにSJTitleSceneで利用しています。 今は、ボタンをクリックしてもNSLogされるだけです。

- (void)createSceneContents {
    
    // Title
    SKLabelNode *titleLabel1 = [SKLabelNode labelNodeWithFontNamed:@"Mosamosa"];
    titleLabel1.text = @"Prototype";
    titleLabel1.fontSize = 28.0f;
    titleLabel1.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMaxY(self.frame) - 80.0f);
    [self addChild:titleLabel1];

    SKLabelNode *titleLabel2 = [SKLabelNode labelNodeWithFontNamed:titleLabel1.fontName];
    titleLabel2.text = @"Quest";
    titleLabel2.position = CGPointMake(CGRectGetMidX(self.frame), titleLabel1.position.y - titleLabel1.frame.size.height - MARGIN);
    titleLabel2.fontSize = titleLabel1.fontSize;
    [self addChild:titleLabel2];

    // New game
    SJTapNode *newNode = [SJTapNode labelNodeWithFontNamed:@""];
    newNode.text = NSLocalizedString(@"New Game", nil);
    newNode.fontSize = 20.0f;
    newNode.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame) - 0.0f);
    newNode.target = self;
    newNode.action = @selector(goNew);
    [self addChild:newNode];
    
    // Continue
    SJTapNode *continueNode = [SJTapNode labelNodeWithFontNamed:newNode.fontName];
    continueNode.text = NSLocalizedString(@"Continue", nil);
    continueNode.fontSize = newNode.fontSize;
    continueNode.position = CGPointMake(CGRectGetMidX(self.frame), newNode.position.y - newNode.frame.size.height - MARGIN);
    continueNode.target = self;
    continueNode.action = @selector(goContinue);
    continueNode.disabled = YES;
    [self addChild:continueNode];

    // Settings
    SJTapNode *settingsNode = [SJTapNode labelNodeWithFontNamed:newNode.fontName];
    settingsNode.text = NSLocalizedString(@"Settings", nil);
    settingsNode.fontSize = newNode.fontSize;
    settingsNode.position = CGPointMake(CGRectGetMidX(self.frame), continueNode.position.y - newNode.frame.size.height - MARGIN);
    settingsNode.target = self;
    settingsNode.action = @selector(goSettings);
    [self addChild:settingsNode];

    // Copyright
    SJTapNode *copyrightNode = [SJTapNode labelNodeWithFontNamed:newNode.fontName];
    copyrightNode.text = NSLocalizedString(@"© 2013 SpriteKit.jp", nil);
    copyrightNode.fontSize = 12.0f;
    copyrightNode.position = CGPointMake(CGRectGetMidX(self.frame), 40.0f);
    copyrightNode.target = self;
    copyrightNode.action = @selector(goCopyright);
    [self addChild:copyrightNode];

}

- (void)goNew {
    NSLog(@"New");
}

- (void)goContinue {
    NSLog(@"Continue");
}

- (void)goSettings {
    NSLog(@"Settings");
}

- (void)goCopyright {
    NSLog(@"Copyright");
}

ごく単純ですが、これでタイトル画面が表示されます。


質素な画面

次は設定画面を作ります。

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

コメント

会話後に他のシーンに遷移する

そろそろお店の中を歩くだけは飽きてきたので、他のシーンに移動したいと思います。
まだまだ足りない機能はあるのですが、今はあくまでプロトタイプなので、細かいのは実際にゲームに組み込む時に実装する予定。

今回からSceneの構成を変更しました。
ベースとしてSJBaseSceneを作成。シーンの情報を定義したjsonファイルの内容を元に、各シーンを読み込みます。

SJBaseScene

- (id)initWithSize:(CGSize)size name:(NSString *)name {
    if (self = [super initWithSize:size]) {
        NSString *path = [[NSBundle mainBundle] pathForResource:name ofType:@"json"];
        NSData *data = [NSData dataWithContentsOfFile:path];
        NSError *error = nil;
        self.sceneData = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&error];
        if (error) {
            NSLog(@"%@", [error localizedDescription]);
        }
    }
    return self;
}

- (void)loadScene:(NSString *)name {
    SKScene *scene;
    if ([name hasPrefix:@"story"]) {
        scene = [[SJStoryScene alloc] initWithSize:self.size name:name];
    }
    [self.view presentScene:scene];
}

- (void)loadNextScene {
    if (self.nextScene) {
        [self loadScene:self.nextScene];
    }
}

以下が今回利用したstory_opening.jsonです。
typestroyとなっているため、SJStorySceneを読み込みます。

{
    "type" : "story",
    "map" : "map_shop",
    "events" : {
        "c1" : {
            "type" : "message",
            "message" : {
                "en" : "hello, world.",
                "ja" : "よくきた、○○よ。待っておったぞ。ここは××研究所。これから旅に出るお主に、託したいものがあって呼んだのじゃ。その宝箱の中身を持って行くがよい。世界の平和を頼んだぞ。"
            },
            "next" : "story_001"
        }
    }
}

また、nextで設定されているのが遷移先のシーンです。
今回はまだ他のシーンがないため、自分自身を指定しています。
これを以下のように、会話が始まる時にnextSceneプロパティに保持します。

SJStroyScene

# pragma mark - SKPhysicsContactDelegate

- (void)didBeginContact:(SKPhysicsContact *)contact {
    SKPhysicsBody *firstBody, *secondBody;
    
    if (contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask) {
        firstBody = contact.bodyA;
        secondBody = contact.bodyB;
    } else {
        firstBody = contact.bodyB;
        secondBody = contact.bodyA;
    }
    
    if ((firstBody.categoryBitMask & playerCategory) != 0) {
        if ((secondBody.categoryBitMask & characterCategory) != 0) {
            SJCharacterNode *node = (SJCharacterNode *)secondBody.node;
            NSString *name = node.name;
            NSDictionary *event = self.sceneData[@"events"][name];
            if ([event[@"type"] isEqualToString:@"message"]) {
                _state = SJStorySceneStateMessage;
                [self messageNode].message = event[@"message"][[SJUtilities lang]];
                [self messageNode].hidden = NO;
                self.nextScene = event[@"next"];

                [[self playerNode] removeAllActions];
            }
            
        }
    }
}

そして、メッセージの表示が終わった時に、loadNextSceneを呼び出します。
loadNextSceneは冒頭のSJBaseSceneで定義されているメソッドで、nextSceneが設定されていればそのシーンに遷移します。

SJStroyScene

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    UITouch *touch = [touches anyObject];
    CGPoint locaiton = [touch locationInNode:[self mapNode]];
    
    switch (_state) {
        case SJStorySceneStateWalk:
            [[self playerNode] moveTo:locaiton];
            break;
        case SJStorySceneStateMessage:
            if ([[self messageNode] hasNext]) {
                [[self messageNode] next];
            } else {
                [self messageNode].hidden = YES;
                _state = SJStorySceneStateWalk;
                [self loadNextScene];
            }
            break;
    }
}

これで、博士との会話が終わると他のシーンに移動するようになりました。


無限ループ

ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps

コメント

SpriteKitではじめる2Dゲームプログラミング Swift対応 (Smart Game Developer)