ゲーム制作フレームワーク不要! 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 系のエンジンを使った方がさくっと作れるんでしょうかね・・・。
詳しい方、おすすめのフレームワークなどありましたら是非教えてください!!

参考リンク

Sysinternals Desktops 日本語化

Desktops 2.0 日本語化パッチ

Desktops の紹介

DesktopsWindows Sysinternals の一部として公開されている、仮想デスクトップ作成ソフトウェアです。
シンプルな作りで、タスクトレイに常駐し、ホットキーで仮想デスクトップを切り替えることができます。
もちろん、マウスを使ってタスクトレイから仮想デスクトップを切り替えることも可能です。

日本語化パッチをダウンロード

Rackhub (Ubuntu 12.04) に RMagick をインストールする

Rackhub の手軽感すばらしいですね。Ubuntu 12.04 をベースに RVM や pythonbrew 等があらかじめ設定されていて、「サーバー(ラック)ごとリセットしてもいいや」くらい雑な感じで扱っています。
そんな Rackhub に RMagick をインストールしようとしてはまったのでメモしておきます。分かってしまえばなんてこと無いのですが・・・。
Rackhub とタイトルに付けていますが、今回はたまたま Rackhub だっただけで、Ubuntu 12.04 系のサーバーであればさくらのVPSだろうがお名前VPSだろうが同様の手順でいけるはずです。

ImageMagick と開発用パッケージをインストールする

$ sudo aptitude install imagemagick
$ sudo aptitude install graphicsmagick-libmagick-dev-compat

実はここではまっていました。RMagick をインストールしようとすると、「MagickWand.h がないよ」とか言われるので、libmagick-dev や libmagickwand-dev 等のパッケージをインストールしてみるもだめ。
結局上記の graphicsmagick-libmagick-dev-compat をインストールしないといけなかったみたいです。

RMagick をインストール

上記のパッケージをインストールできればあとは簡単です。

$ gem install rmagick

これで RMagick を無事インストールできるはずです。

利用できるか確認する

実際に RMagick を利用してみます。

require 'rubygems'
require 'RMagick'
image = Magick::Image.read('large.jpg').first
p image
image.resize_to_fit!(100, 100)
image.write('small.jpg')

これで large.jpg を small.jpg にリサイズできていれば OK です。

快適!! お名前VPS(KVM)にUbuntu12.04をインストールし、Nginx と Unicorn で Rails アプリを動かす

お名前 VPS (KVM) 2GB プランを契約したので、Ubuntu 12.04 LTS をインストールしてみました。
ついでに Rails が動く環境を作るところまでやったので、その過程を共有します。

初期セットアップ

お名前 VPS を契約して最初にコンパネにアクセスすると CentOS 6.2 がデフォルトで、初期パスワードの入力を求められます。とりあえず最初は CentOS をインストールしないといけないみたいです。おとなしくパスワードを入れて次に進みます。
SSH-Key の選択も出てきますが、デフォルトのままにしました。

Ubuntu 12.04 LTS をインストールする

CentOS のインストールが終わったら、コンパネから「OS再インストール」タブを選択し、Ubuntu 12.04 LTS (64bit) を選択します。
その他の設定はお好みで。今回は以下のようにしました。

  • Ubuntu 12.04 LTS 64bit
  • Virtio: ON
  • Disk: 200GB + 0GB

コンソール画面

コンソール画面でインストール設定を行います。以下のように設定しました。

  • Install
  • Japanese (English とかのほうがいいかも。文字化け対策に。)
  • 地域: 日本
  • キーボード
    • Japan
  • ネットワークが自動設定される
  • ホスト名はデフォルト
  • Ubuntu アーカイブのミラー
    • 日本 (jp.archive.ubuntu.com)
  • HTTP Proxy なし
  • ユーザーアカウント
    • Full Name: お好みで。
    • Account Name: お好みで。便宜上 user とする
  • パスワード: ****
  • Home ディレクトリの暗号化: NO
  • Timezone: Asia/Tokyo
  • ディスクのパーティショニング
    • ディスク全体を使う
    • 仮想ディスク1 を選択
    • 既存のデータを削除: YES
    • ディスクに変更を書き込む: YES
  • インストールが始まります
  • アップデートの設定
    • 自動的にアップデートしない (Default)
  • ソフトウェアの選択
    • デフォルトのまま (すべて選択しない)
  • GRUB ブートローダのインストール
    • YES
  • システム時間を UTC にするかどうか
    • いいえ
  • インストール完了
    • コンパネ画面上部の「取り出す」ボタンでインストールディスクを排出しておく


インストールはこんなかんじです。コンパネのコンソールだと何かと不便なので、ssh 接続できるように設定します。

$ sudo aptitude install ssh
$ sudo service ssh start

(公開鍵認証とかにしたほうがいいと思うけど割愛。)

環境を整える

ここから Mac の Terminal で作業していきます。
まずはアップデート。インストール時にアップデートが入ってると思うけど一応。

$ ssh user@hostname (サーバーに接続)
$ sudo aptitude update
$ sudo aptitude upgrade

Vim が入ってなかったのでインストール

$ sudo aptitude install vim
ファイアーウィールの設定

とりあえず ssh のみ許可。

$ sudo ufw status
状態: disabled
$ sudo ufw enable
$ sudo ufw default REJECT
$ sudo ufw allow 22/tcp

パッケージにインストールされているものから選ぶ事もできる。

$ sudo ufw app list
OpenSSH

$ sudo ufw allow "OpenSSH"
ビルド環境

Ruby などのビルドに必要なパッケージをインストールします。

$ sudo aptitude install build-essential libreadline-dev libssl-dev zlib1g-dev libssl-dev libssl1.0.0 libsqlite3-dev  git-core curl
RVM

Ruby 環境は RVM で整えます。

$ curl -L https://get.rvm.io | bash -s stable --ruby
$ source /home/user/.rvm/scripts/rvm
$ echo "source /home/user/.rvm/scripts/rvm" >> .bash_profile
RailsUnicorn

とりあえず Rails あたりをインストール。

$ gem install rails unicorn

Gemfile にも適切に gem を書き込んでおく。

Rails アプリを立ち上げる

Rails アプリのディレクトリに移動して以下のように設定する

$ bundle install
$ bundle exec rake db:setup
$ bundle exec rake assets:precompile
$ bundle exec unicorn_rails -E production -D

こうすると 127.0.0.1:8080 でサーバーが起動します。

Nginx

フロントには nginx を使うので、nginx をさくっとインストールします。

$ sudo aptitude install nginx
$ sudo ufw allow "Nginx Full"
$ sudo vim /etc/nginx/conf.d/backend.conf
upstream backend {
    server 127.0.0.1:8080;
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_set_header HOST $host;
       	proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_pass http://backend;
        proxy_redirect off;
    }
}

今回はログとか asset の設定をしてないので、そのへんをちゃんと設定する。

おわり

かなり長くなってしまったので、端折ってる部分もありますが、だいたいこんな感じでした。
安くて高性能な VPS が簡単に手に入るようになったので遊び倒したいです。

オマケ

Pythonbrew をこっそりインストール

上記の「ビルド環境」でインストールしたもの以外にも必要なパッケージがあるので、それらをインストールしてから Pythonbrew をインストールします。

$ sudo aptitude install libncurses-dev libgdbm-dev tk-dev libdb4.8-dev
$ curl -kL http://xrl.us/pythonbrewinstall | bash
$ echo "[[ -s $HOME/.pythonbrew/etc/bashrc ]] && source $HOME/.pythonbrew/etc/bashrc" >> .bash_profile
$ source ~/.pythonbrew/etc/bashrc
$ pybrew install 2.7.3
$ pybrew switch 2.7.3

build.log に以下のようなメッセージが出ているけど、おそらく古すぎて使うことがないので無視。

Python build finished, but the necessary bits to build these modules were not found:
bsddb185 dl imageop
sunaudiodev

これで Python でもたっぷり遊べます!

超便利な Python モジュール requests を使って OAuth 認証し、Zaim と連携する方法


「HTTP for Humans」でおなじみの requests モジュールが 0.12.0 で OAuth に対応し、0.12.1 では HTTP レスポンスが JSON だった場合、オブジェクトへ自動変換できるようになりました。かなり手軽に Web サービスの API をたたけるようになりました。
そこで、試しに Zaim と連携してみたいと思います。

requests のインストール

pip install requests

または

easy_install requests

アプリケーションを登録

Zaim Developers にアクセスし、「アプリケーションを登録」してください。
Consumer Key や Consumer Secret が発行されるのでそれらをメモしておきます。

Access Token を発行する

Python スクリプトから OAuth 認証を要求し、zaim のログイン画面でログイン後、Access Token が発行されます。
以下が Access Token 取得用 Python スクリプトです。
スクリプトを実行すると、認証用 URL が表示され、「What is the PIN?」と聞かれますので、oauth_verifier の値を入力します。
ちなみに oauth_verifier は画面には表示されず、ブラウザで「ソースを表示」すると oauth_verifier が書いてあります。
consumer_key, consumer_secret の設定をお忘れ無く。

# coding: utf-8
import urlparse

import requests
from requests.auth import OAuth1


consumer_key = u"YOUR CONSUMER KEY"
consumer_secret = u"YOUR CONSUMER SECRET"

request_token_url = u"https://api.zaim.net/v1/auth/request"
authorize_url = u"https://www.zaim.net/users/auth"
access_token_url = u"https://api.zaim.net/v1/auth/access"
callback_uri = u"https://www.zaim.net/"


def oauth_requests():
    # Request Token
    #   Web サービスとして実装する場合は、callback_uri に認証が完了したことが通知される。
    #   callback_uri にリダイレクトする際、oauth_verifier がパラメータとして渡される。
    auth = OAuth1(consumer_key, consumer_secret, callback_uri=callback_uri)
    r = requests.post(request_token_url, auth=auth)
    request_token = dict(urlparse.parse_qsl(r.text))
    
    # Authorize
    print "Auth link:"
    print "%s?oauth_token=%s" % (authorize_url, request_token["oauth_token"])
    print

    oauth_verifier = unicode(raw_input("What is the PIN? "))

    auth = OAuth1(consumer_key, consumer_secret, request_token["oauth_token"], request_token["oauth_token_secret"], verifier=oauth_verifier)
    r = requests.post(access_token_url, auth=auth)
    
    access_token = dict(urlparse.parse_qsl(r.text))
    return access_token

if __name__ == "__main__":
    print oauth_requests()

Zaim と連携する

Access Token がとれたら、Zaim API にアクセスできるようになります。
consumer_key, consumer_secret, oauth_token の設定をお忘れ無く。
以下のスクリプトで情報表示、支払い情報の入力を行っています。
requests のおかげで楽ちんですね。あとはリファレンスを参照しながら遊んでみましょう。

# coding: utf-8
from datetime import datetime

import requests
from requests.auth import OAuth1


consumer_key = u"YOUR CONSUMER KEY"
consumer_secret = u"YOUR CONSUMER SECRET"
access_token = u"ACCESS TOKEN"
access_secret = u"ACCESS TOKEN SECRET"

auth = OAuth1(consumer_key, consumer_secret, access_token, access_secret)


def date_now():
    return datetime.now().strftime("%Y-%m-%d")


def genre(lang="ja"):
    endpoint = "https://api.zaim.net/v1/genre/pay.json"

    data = {"lang": lang}
    r = requests.post(endpoint, data=data, auth=auth)

    return r.json


def create_payment(price=0):
    endpoint = "https://api.zaim.net/v1/pay/create.json"

    data = {
        "category_id": "101",
        "genre_id": "10101",
        "price": unicode(price),
        "date": date_now(),
        "comment": "test",
        }

    r = requests.post(endpoint, data=data, auth=auth)
    return r.json


def money():
    endpoint = "https://api.zaim.net/v1/money/index.json"

    data = {"limit": "10",}

    r = requests.post(endpoint, data=data, auth=auth)
    return r.json


def credentials():
    endpoint = "https://api.zaim.net/v1/user/verify_credentials.json"

    r = requests.post(endpoint, auth=auth)
    return r.json


if __name__ == "__main__":
    print "-" * 100
    print genre()
    print "-" * 100
    print create_payment(1234)
    print "-" * 100
    print money()
    print "-" * 100
    print credentials()

まとめ

requests モジュールのおかげで、REST API へのアクセスが簡単になりました。
お気に入りの Web サービスと連携したい場合は、requests を利用してみてください。

TCPEye 日本語化

TCPEye 1.0 日本語化言語ファイル

TCPEye の紹介

TCPEyeTCPUDP の接続状況を表示するソフトウェアです。どのプロセスがどの IP に接続しているのかや、どの国に接続しているかなどを表示させることができます。プロセスのネットワークに接続を確認することで、怪しいプロセスの早期発見や、不必要な接続を切断したりすることができます。

日本語化言語ファイルをダウンロード

PC 専用メガネ「JINS PC」をどう評価すればいいのかわからない

PC専用メガネなる「JINS PC」をJINS オンラインショップで購入しました。


ブルーライトから眼を守る
ディスプレイの文字をクリアに

http://www.jins-jp.com/functional/pc.html

1日中パソコンの前で作業することが多く、目の疲れが気になっていたので即予約&購入しました。
使い始めてまだ3日目しかたってませんが、手にとって感じたこと、それは・・・。

うん、サングラスだよね、これ。

色のうっすいサングラスです。確かに強すぎる光を遮ってくれそうです。これがブルーライトの光を軽減してくれるのでしょう。それからディスプレイの眩しさを軽減してくれるので、ディスプレイの文字を読みやすくなります。
ということでうたい文句は間違ってはないと思います。

寝不足?ビールでも飲んだ?

レンズがうっすらサングラスぎみなので、目の周りにクマができたように見えたり、お酒を飲んで少し顔色が変わった時のように見えたります。見慣れるとそこまで気になるほどではないですが、第一印象は違和感を感じます。職場で使うには少し勇気がいるかもしれません。

評価

目の疲れが軽減されたなどの変化をどうやって計ったらいいんでしょうか。まだ使い始めて3日目ですが「おっ」と思うような変化はとくにありません。
顔の印象が若干悪くなる点が一番気になるポイントでした。気にしすぎでしょうか。ご購入を検討中の方は店頭で手にとって判断されることをおすすめします。