設定シーン

引き続き軽いお題で、設定画面です。
言語の日英切り替えと音の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

コメント

TODO

目標のゲームを作るために必要そうなものをメモ。

イベント

  • ✔ prompt: 名前の入力など
  • ✔ confirm: はい・いいえ
  • ✔ replace: キャラクターを入れ替える

シーン

  • ✔ title: ゲームタイトル
  • ✔ settings: 言語・音声設定やライセンス表示
  • ✔ chapter: 章切り替え
  • ✔ ending: クリア後の画面
  • battle: 戦闘
  • menu: 装備など
  • continue: ゲームの再開

その他

  • utility: セーブ・ロード
  • utility: 音

年内を目指していたけど、厳しそうなので年度内を目標に。

コメント

触れた人と会話する

前回から少し時間が開いてしまいましたが、今回はキャラクターに話をさせてみます。

event.jsonファイルでデータを定義しています。

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

接触時のdelegateメソッドで、playerNodeがc1という名前のNodeに触れた時に、メッセージの内容を表示するようにしています。

- (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 *e = [self event][name];
            if ([e[@"type"] isEqualToString:@"message"]) {
                _state = SJShopSceneStateMessage;
                [self messageNode].message = e[@"message"][[SJUtilities lang]];
                [self messageNode].hidden = NO;
            }
        }
    }
}

メッセージの表示は、新たに追加したSJMessageNodeに担当させています。

SKLabelNodeが複数行表示に対応していないため、行数分並べて表示するという泥臭い実装になっています。 特別なことをしていない上に無駄に長いため、引用はやめておきます。

無事、キャラクターに話しかけることができるようになりました。


唐突な会話

次は他のシーンへの遷移を実装します。

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

コメント

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