これも昔からお馴染み、「君の名前は?」イベント。
顔見知りなのに名前を知らない不思議。
Confirmと同じくUIAlertViewで済ませます。
まずは設定ファイル。
{
"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に保存します。
そして、保存された内容を使ってメッセージを表示します。
アラート表示は以下のような実装。
- (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で済ませます。
設定ファイルは以下のとおりです。
{
"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をタップされるとc1y
、Noをタップされるとc1n
に進むようにします。1
- (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
引き続き軽いお題で、設定画面です。
言語の日英切り替えと音の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;
}
完成画面はこちら。
今回はあえて、Sprite Kitを使っていますが、UIKit(UITableView)を使った方が確実に楽なので、そちらをオススメします。
ソースコード: sj-prototype-apps/SJRolePlaying at master · tnantoka/sj-prototype-apps
今回は軽いお題で、タイトル画面を作ります。
リリース時には凝った演出があった方がよいかもしれませんが、現状ではタイトルとボタンを並べるだけです。
ボタンはSJTapNodeというクラスで表現しています。
touchesEnded:withEvent:
が発生した時にdisabledじゃなければ、targetのactionを呼び出す単純なNodeです。
なお、タップ中やdisabled時はcolorBlendFactorを操作してグレーになるようにしています。
- (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ファイルの内容を元に、各シーンを読み込みます。
- (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
です。
typeがstroy
となっているため、SJStorySceneを読み込みます。
{
"type" : "story",
"map" : "map_shop",
"events" : {
"c1" : {
"type" : "message",
"message" : {
"en" : "hello, world.",
"ja" : "よくきた、○○よ。待っておったぞ。ここは××研究所。これから旅に出るお主に、託したいものがあって呼んだのじゃ。その宝箱の中身を持って行くがよい。世界の平和を頼んだぞ。"
},
"next" : "story_001"
}
}
}
また、nextで設定されているのが遷移先のシーンです。
今回はまだ他のシーンがないため、自分自身を指定しています。
これを以下のように、会話が始まる時にnextSceneプロパティに保持します。
# 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が設定されていればそのシーンに遷移します。
- (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