ゲーム制作フレームワーク不要! iOS ネイティブのノベルゲームアプリの作り方

iOS でノベルゲームを作る場合、ベストな方法が分からなかったので、とりあえず愚直な実装をしてみることにしました。
ゲーム制作フレームワークや、イケてるグラフィック効果など使っていません。言語は通常の Objective-C です。
今回作成したサンプルはこちらからダウンロード可能です。

画面イメージ


仕様的なもの

  • 縦画面
  • 画面をタップするとゲームスタート
  • 画面下部に台詞などのテキストを表示する
  • 画面全体に背景画像を表示する
  • タップすると次のテキスト・画像を表示する
  • 最後までいくと最初にもどる

画面に表示される一連のオブジェクトをまとめたモデルを作る

今回はサンプルなので、表示するテキストと、背景画像のみをプロパティに持ちます。
実際のゲームでは立ち絵などのプロパティも必要になってくるでしょう。たぶん。

// NVMessage.h
#import <Foundation/Foundation.h>
@interface NVMessage : NSObject
@property (copy, nonatomic) NSString *text;
@property (copy, nonatomic) NSString *bgImageName;
@end

// NVMessage.m
#import "NVMessage.h"
@implementation NVMessage
@end

アプリの起動

今回は Interface Builder を使用せず、コードでビューを作っていきます。
そのため AppDelegate には次のように記述します。

// NVAppDelegate.h
#import <UIKit/UIKit.h>
@interface NVAppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@end


// NVAppDelegate.m
#import "NVAppDelegate.h"
#import "NVViewController.h"
@implementation NVAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.rootViewController = [[NVViewController alloc] init];
    [self.window makeKeyAndVisible];

    // Override point for customization after application launch.
    return YES;
}
@end

画面を表示する

テキスト・背景を表示する画面を作ります。
特に難しいことはしていないのですが、テキストを実際のゲームっぽく一文字ずつ順に表示する処理を実装したかったので、
その部分が少し複雑になっています。

画面処理の流れは、ビュー生成 (loadView) => ストーリー生成 (generateStory) => ユーザー反応待ち (buttonPressed:) => 次のメッセージ表示 という感じです。

以下、NVViewController のソースコードです。

// NVViewController.h
#import <UIKit/UIKit.h>
@interface NVViewController : UIViewController
@end


// NVViewController.m
#import <QuartzCore/QuartzCore.h>
#import "NVViewController.h"
#import "NVMessage.h"

@interface NVViewController ()
@property (strong, nonatomic) UITextView *messageView;
@property (strong, nonatomic) UIImageView *bgImageView;

@property (assign) BOOL messageTextAnimating;
@property (strong, nonatomic) NSArray *messages;
@property (assign) NSInteger messageIndex;
@end

@implementation NVViewController

- (void)loadView {
    [super loadView];
    
    self.view.backgroundColor = [UIColor blackColor];
    
    UIImageView *bgImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:bgImageView];
    self.bgImageView = bgImageView;
    
    
    // メッセージ表示枠を作る
    CGFloat messageHeight = 100;
    CGFloat messageMargin = 10;
    CGRect messageFrame = CGRectMake(messageMargin, self.view.frame.size.height - messageHeight - messageMargin, self.view.frame.size.width - (messageMargin * 2), messageHeight);
    
    // メッセージ表示の背景
    UIView *messageBgView = [[UIView alloc] initWithFrame:messageFrame];
    messageBgView.layer.borderWidth = 1.0f;
    messageBgView.layer.borderColor = [UIColor whiteColor].CGColor;
    messageBgView.backgroundColor = [UIColor blackColor];
    messageBgView.alpha = 0.7f;
    [self.view addSubview:messageBgView];
    
    // メッセージテキスト表示ビュー
    UITextView *messageView = [[UITextView alloc] initWithFrame:messageFrame];
    messageView.editable = NO;
    messageView.userInteractionEnabled = NO;
    messageView.textColor = [UIColor whiteColor];
    messageView.backgroundColor = [UIColor clearColor];
    messageView.font = [UIFont boldSystemFontOfSize:14.0f];
    [self.view addSubview:messageView];
    self.messageView = messageView;
    
    // メッセージのタップ可能領域
    UIButton *button = [[UIButton alloc] initWithFrame:messageFrame];
    [button addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside];
    button.backgroundColor = [UIColor clearColor];
    [self.view addSubview:button];
}

- (void)buttonPressed:(id)sender {
    if (self.messageTextAnimating) {
        // メッセージを表示しかけのときはアニメーションを停止
        self.messageTextAnimating = NO;
        return;
    }
    
    if ([self.messages count] <= self.messageIndex) {
        // 最初に戻る
        self.messageIndex = 0;
        self.bgImageView.image = nil;
    }
    
    if ([self.messages count] > self.messageIndex) {
        NVMessage *m = self.messages[self.messageIndex++];
        
        // 背景画像名が設定してある場合
        if ([m.bgImageName length] > 0) {
            [UIView animateWithDuration:0.1 animations:^{
                // 前の画像をフェードアウト
                self.bgImageView.alpha = 0.5f;
            } completion:^(BOOL finished) {
                // 新しい画像をフェードイン
                [UIView animateWithDuration:0.2 animations:^{
                    self.bgImageView.image = [UIImage imageNamed:m.bgImageName];
                    self.bgImageView.alpha = 1.0f;
                }];
            }];
        }
        else {
            self.bgImageView.image = nil;
        }
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
            self.messageTextAnimating = YES;
            for (NSInteger i = 0; i <= [m.text length]; i++) {
                if (!self.messageTextAnimating) {
                    // アニメーションが停止されてたら全テキストを表示する
                    dispatch_async(dispatch_get_main_queue(), ^{
                        self.messageView.text = m.text;
                    });
                    break;
                }
                
                // インデックス位置までのテキストを表示する
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.messageView.text = [m.text substringToIndex:i];
                });
                
                // 次の文字を表示するまでの待ち時間
                [NSThread sleepForTimeInterval:0.05];
            }
            self.messageTextAnimating = NO;
        });
    }
}

- (NSArray *)generateStory {
    NVMessage *m1 = [[NVMessage alloc] init];
    m1.text = @"私「こんにちは」";
    m1.bgImageName = @"bg_001.jpg";
    NVMessage *m2 = [[NVMessage alloc] init];
    m2.text = @"彼「さようなら」";
    m2.bgImageName = @"bg_002.jpg";
    NVMessage *m3 = [[NVMessage alloc] init];
    m3.text = @"私「お待ちください。まだ話は終わっておりません。」";
    m3.bgImageName = @"bg_001.jpg";
    
    return @[m1, m2, m3];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.messages = [self generateStory];
    self.messageIndex = 0;
    self.messageTextAnimating = NO;
}

@end

完成!!

立ち絵や選択肢の実装も無く、ゲームと言ってはいけないような気もしますがとりあえず完成です!

実際のところ iOS のノベルゲーム開発ってどうやるんでしょうか

この分野は余り詳しくないのですが、通常だとゲーム制作フレームワークなどを使うのでしょうか? 吉里吉里系や NScripter 系のエンジンを使った方がさくっと作れるんでしょうかね・・・。
詳しい方、おすすめのフレームワークなどありましたら是非教えてください!!

参考リンク