ゴマちゃんフロンティアV2へようこそ!

本ブログはゲーム統合開発環境「Unity」を使用したゲーム開発の日記です!
ときどきイラストを描いたり、写真を交えた日記も書いています!

【開発環境】

OS Windows7 SP1 64bit
CPU Intel(R) Core(TM) i7-4770 CPU @ 3.40GHz
メモリ 12GB
グラボ GeForce GTX 645
使用ソフト Microsoft Visual Studio(C#)
Blender
GIMP

旧ブログの記事について

本ブログは2015年9月にFC2ブログから移転しております。
過去記事につきまして、「開発方針が変わっている」「Unityの仕様が現状と異なる」等、そのままでは不適切と判断しました。
なので、知識・技術面で重要そうな記事のみをピックアップし、再編集して新ブログに追加している段階です。
Googleのインデックスの関係上、検索内容とブログ内容に差異が発生する可能性がありますので、ご了承ください。
また「前のブログのこの記事読みたい!」というものがありましたら、ご連絡頂ければ追加致します!

お問い合わせフォームはこちら

【開発メモ】敵キャラクター作成とロジックに関する考察

というわけで、最近 Twitter の bot 制作の話ばかりでしたが、今回はゲーム開発のお話です!

テーマは「敵キャラクターとロジックに関するお話」です!
定期的にこんな話は載せていますが、どれも中途半端な実装とロジックなので、もう一度ベストな形を考え直す意味合いです。特に敵キャラクターのロジック部分はしっかり考える必要があります。

結論から言うと、「移動や攻撃などの役割毎にコンポーネントとして分解する」ようにするのが目的です。
なるべくロジックを分解し、パーツとして組み合わせることができるうな形が理想です。

敵キャラクターの作成

自作ゲームである以上、敵キャラクターも全て自分で考えます。
ロジックどうこうの前に、敵キャラクターを用意しないと話になりません。

とはいえ、今まともな敵と言えるものは「バブぴょん」くらいです。
開発初期の方に作ったので設定も何もなく、ロジックもかなり適当です。

shot2ss20170122094910458

敵キャラクターを作っていく場合、相応に種類が必要になるので、ある程度設定を考え、一貫性を持たせたほうがモデリングやモーション入れもラクです。
今考えている設定に「共通で結晶や角がある」「雷と氷を放つ」があるので、とりあえずそれを盛り込んで作りました。
前者は角ばっている方がモデリングが楽だからで、後者の「雷と氷」というのは、「海(≒水)を脅かす属性」として、一番メジャーで妥当かと思ったためです。

そんなわけで、ざっくりと2匹作ってみました!
上記のバブぴょんは「敵っぽくなさ」に定評がありましたが、今回のはそれなりに敵っぽくなっております!

shot2ss20170122093840735

1体は二足歩行の至ってシンプルな見た目です。
槍状の結晶による突きと角からのビームを放ち、敵軍団の主力となるような敵を目指します。
暫定的な名前として「レイドラン」と付けています。

shot2ss20170122093938413

もう1体は地を這う幽霊っぽい見た目です。
動作は遅めですが、両手で薙ぎ払う攻撃と防御を得意とする敵を目指します。
幽霊っぽく、目はパーティクルで表現しています。

ロジック部分の設計について

敵キャラクターが取る行動について、役割ごとにざっくりと洗い出してみます。

【索敵】
・索敵をしない (プレイヤーの行動に影響されない)
・前方を索敵
・周囲を索敵

【移動】
・移動しない
・常に前進
・一定の範囲を往復
・プレイヤーを追跡

【ジャンプ】
・ジャンプしない
・一定間隔でジャンプ
・攻撃時のみジャンプ
・常に浮いている

【攻撃】
・攻撃しない
・常に一定間隔で攻撃
・プレイヤーが索敵範囲内にいる場合に攻撃

概ねこんな感じでしょうか。
このロジック毎にクラスを作成し、敵キャラクターにコンポーネントとして追加するだけで設定が可能な状態を目指します。キャラクターの固有クラスでロジックを作成すると使い回しがきつくなる上、クラスの役割が肥大化するので避けたいところです。
その分クラスが多くなりますが、命名規則を決めたり(頭文字をAttack~、Move~にするなど)、名前空間を指定するなどでカバーします。

この他、キャラ固有の特殊能力などは個別のサブクラスに実装してしまいます。
また、各ステージのボスは専用のロジックを作成する予定です。

あくまで自己流の設計なので、これがベストとは言えないと思いますが、そこまでたくさんキャラクター(とロジック)作るアイデアも時間もないので、これで行ってみます!

まとめ

そんなわけで、敵キャラクターとロジックについて考えてみました!
実装の際のポイントは
・役割とそのロジック毎にクラスを作成
・コンポーネントとして付け替え/設定ができるように
・命名規則と名前空間をしっかり決めておく

あたりでしょうか!

次回から作っていくつもりですが、最近出張やら外泊やらが多いので、そこそこ先の話になってしまうかもしれません。
妥協するところは妥協しないといつまで経っても完成しないので、そのあたりの線引きも大切ですね。

【Twitter】ランパの台詞botを作成してみる その2

というわけで、今回は「ランパの台詞botを作る」第2弾です。
前回の記事はコチラ!

【Twitter】ランパの台詞botを作成してみる その1

今回は台詞集から出力するテキストを抜き出し、ツイートするところまでやってみます!
ついでに作っている上で苦労していることも紹介します。

台詞の入力

とりあえず「オープニング~ステージ3-2」までと、その他回収できた台詞を書き出してみました!

20170116_02

見直してみると結構喋りますね。
ステージ3-2まででも3000文字近くあります。
ハマグリほどではありませんが、主人公が喋らないのでその影響もあるようです。
一方で全く喋らない(≒登場しない)ステージもそれなりにあるので、各ステージ満遍なく喋るというよりは、1ステージでの回数が多いといった印象です。

それにしても、3点リーダーが尋常でなく多いです。

DSC_0281

恐らく会話ウィンドウ的な都合があるのだと思いますが、それを差し置いても多いです。
実際にツイートさせる時は適度に削除しないと見にくくなってしまいます。

ゲーム自体の対象年齢故にひらがなも多いですが、簡単な漢字とカタカナは含まれているので、そこまで読みにくくはないと思います。
ただ句読点が一切ない上、上記の3点リーダーを削除してツイートする兼ね合いもあるため、適度に句読点は入れています。
それ以外は原文ママで問題ないと思われます。

twitterアイコンの作成

デフォルトのたまごアイコンでは味気ないので、アイコン用にランパを描いてみました。
ステージ選択のアイコンを元にしました。

20170116_rampa_icon1

うーん・・・また何とも言えない出来ですが、まあ及第点でしょう。
まあアイコンなんて小さいものなので、割と大雑把なくらいがちょうどよかったりします。

個人ページの背景は・・・後回しにします(ぁ
今の自分のイラスト力では描ける気がしないので・・・。

bot用PHPファイルの修正

前回ほぼコピペだったPHPファイルを修正します。
自分は手続き型っぽく書くのが嫌いなので、意味もなくクラスをインスタンス化するようにしました。
ツイートする部分は同じですが、ランパの台詞の抽出と整形部分を大きく変えています。

ツイートの際の理想の形式は「ランパの台詞 (改行)【発言した場面】」です。
また、台詞が複数行になることもあるので、その点も考慮する必要があります。

<?php

require_once("twitteroauth-master/autoload.php");
require_once('twitteroauth-master/src/TwitterOAuth.php');

use Abraham\TwitterOAuth\TwitterOAuth;

/**
 * ランパの台詞集から台詞を抽出・ツイートするクラス
 */
class TwitterRampaBot {

    private $customer_key;
    private $customer_secret;
    private $access_token;
    private $access_token_secret;

    const file_path = 'rampa_serifu.txt';
    const max_tweet_len = 140;

    /**
     * コンストラクタ
     */
    public function __construct() {
        $this->customer_key = "customer_key";
        $this->customer_secret = "customer_secret";
        $this->access_token = "access_token";
        $this->access_token_secret = "access_token_secret";

        mb_internal_encoding('UTF-8');
    }

    /**
     * つぶやきを送信する
     */
    public function tweet() {
        $message = $this->getTweetWord();

        // 文字数チェック
        if (self::max_tweet_len < mb_strlen($message)) {
            trigger_error('ツイート可能な文字数を超えています。', E_USER_ERROR);
            exit;
        }

        // サーバにつぶやきを送信
        $connection = new TwitterOAuth($this->customer_key, $this->customer_secret, $this->access_token, $this->access_token_secret);
        $connection->post("statuses/update", array("status" => $message));
    }

    /**
     * ツイート用の文字列を取得・整形する
     *
     * @return string
     */
    private function getTweetWord() {
        // テキストファイルから文字列読み込み
        $input_lines = file(self::file_path);
        $words = array();

        // 先頭 # の行を取得するまでループ
        while(true) {
            $line_num = rand(0, count($input_lines));

            // 先頭が # で始まる行を抽出
            if (preg_match('/^\# .*$/', $input_lines[$line_num])) {
                // # 以降を場面として取得
                $scene = $this->excludeLineFeed($input_lines[$line_num]);
                $scene = str_replace('# ', '', $scene);

                // 複数行の台詞を全て取得するまでループ
                for ($i = $line_num + 1; ; $i++) {
                    // 文字列か空or改行コードのみの場合は終了
                    if (empty($input_lines[$i]) || preg_match('/^' . PHP_EOL . '$/', $input_lines[$i])) {
                        break;
                    }

                    $words[] = $this->excludeLineFeed($input_lines[$i]);
                }
                break;
            }
        }

        // 台詞と発言場面を設定
        $message = '';
        foreach ($words as $word) {
            $message .= $word . PHP_EOL;
        }

        $message .= '【' . $scene . '】';

        return $message;
    }

    /**
     * 指定した文字列から改行コードを除外する
     *
     * @param string $str
     * @return string
     */
    private function excludeLineFeed($str) {
        return str_replace(PHP_EOL, '', $str);
    }
}

$twitterRampaBot = new TwitterRampaBot();
$twitterRampaBot->tweet();

exit;

?>

GitHub から落とした「TwitterOAuth」を使用していますが、バージョンによってやり方に差があるようです。
自分も他サイトのコピペでは全然動きませんでした。

あれこれ考えた結果、ファイルから取得した後、正規表現で取捨選択するのが最も手っ取り早かったです。
テキストファイルから抽出した文字列から、以下の条件で抽出します。
・# から始まる行を発言場面として抽出
・#以下から次の空白行 (実際は改行コード) までを台詞として抽出
・空白行は抽出しない

台詞集となるテキストファイルもそれに則って書いていきます。
サンプルとして、いくつか台詞を記載します。

# ステージ1-3
・・・どうしてこんなところに・・・
それに何でたたかっていたんだ・・・
うう・・・思い出そうとするとアタマがイタイ・・・
もしかして・・・ボクのきおく・・・

# ステージ1-4
あ・・・うん!!
スタフィーのボクをたすけたいって思う気もちが
ボクのへんしんのチカラ、『ドランパ』を思い出させてくれたんだ・・・

# ステージ2-2
あ、そうだ!ボクとチカラをあわせて下からゴォォォーってやってみる?
ドランパで、上ボタンをおしながらゴォォォーってできるよ
ためしてみて!

注意点として、Linux 上から動作させる関係上、改行コードは「LF」にしておく必要があります。
サクラエディタであれば保存時に指定できます。

かなり効率の悪い処理ですが、何度も実行するような処理ではないので、速度はあまり気にしません。
他にもいろいろ考えましたが、.ini は設定ファイルの趣が強く、このためだけに yaml や json のパーサを入れるのも嫌なので、とりあえずこの形でいってみようと思います!

苦労している点

プレイ日記に片足突っ込みつつ、製作している上で苦戦していることを紹介します。

データが消える

ある意味最大の壁です。
本体がダメなのか、ソフトが古いためか、プレイ中に接触不良→次回起動時にデータ消失ということがあります。
特にカートリッジを抜いておくと、次回起動時に差しても高確率で認識しません。

スロットからカートリッジを出さなければ順調なので、差しっぱなしにすることでカバーしていますが、当分の間3DSがスタフィー専用機となることを意味します。
最近3DSはほとんど起動していないので、あまり実害はないと思われますが、MHXX発売までには完成させたいところです。

ステージクリア後にオートセーブ

カートリッジ故にセーブは高速で、普通にプレイする分には問題ありません。
ただ、ランパの台詞を取り逃してしまったり、間違えていた場合、ステージをクリアするまでにリセットしないとアウトです。
ストーリーが進んでしまうと読み直すことができないので、漏れなくニューゲームとなります。
後半のステージでこれをやってしまうと悲惨なので、最も注意すべき人的エラーですね。

会話後に再度会話をすると内容が異なる

ステージの所々に会話イベントがありますが、会話後に再度キャラクターと会話すると、内容が異なる場合があります。
ゲーム的には素晴らしい作り込みですが、台詞をまとめる上では注意が必要です。

DSC_0283

しかも再会話時の台詞は長いものが多いです。
これも上記のオートセーブと同じく、気付かずにストーリーを進めてセーブされると、最初からやり直す羽目になりかねません。

まとめ

そんなわけで、ランパbotの作成日記その2でした!
まだ途中までしか作っていませんが、アカウントを作ってしまった上、完成がいつになるか分からないので、今のレパートリーで試運転してみようと思います。
cron で1日2回、12時間置きにツイートさせるように設定しています。

ひとまず問題なく稼働しているみたいです。
あとはランパの台詞を逃すことなく追加していけば完成すると思われます!

【Twitter】ランパの台詞botを作成してみる その1

【2017/01/16】
サンプルソースを修正しました。

というわけで、今回はタイトル通り、Twitterのbot作成に挑戦してみます!
作るのは伝説のスタフィーに登場するキャラクター「ランパ」の台詞集です!

ranpa31

上のような、宇宙服を着たウサギのようなキャラです。
(自分が描いたイラストなので実物とは異なります)
「ランパ星」というまんまな星の王子で、いろいろな動物に変身することができます。
「キャラクターとしての魅力は?」と言われれば・・・回答が難しいところですが、自分は「見た目」と「至って普通な性格」と返します。
主人公であるスタフィーとの変身形態の1つにアザラシがあるので、むしろそっちに惹かれたりしていましたが・・・。

DSC_0278

そんなランパの台詞集、誰も作っていなかったので、ネタとしてのポストは空いています。
前からbotを作ってみたかったのもあるので、どんな感じなのかやってみました!

Twitterアカウントの作成

まずはbot用のTwitterアカウントを作成します。
マイアカウントでログインしっぱなしなので一度ログアウトし、新しくアカウントを作成します。

新規アカウントのためにはメールアドレスが必要ですが、bot専用のアカウントを作るのも嫌なので、Gmailのエイリアス機能を使用します。
[アドレス]+[任意文字列]@gmail.com でメールを自動的に転送してくれるらしいです。
細かいことはぐぐればたくさんでるので割愛します。

shot2ss20161223224502738

「ランパ」とだけ打つと、歯の矯正手術とかそっち系のばかりヒットするため、頭にスタフィーと入れておきます。
ちなみにランパのスペルは「Rampa」らしいです。
海外版では「bunston」と全く別名だったりしますが、とりあえずランパで行きます!

shot2ss20161223224725777

電話番号認証の後、ユーザIDを入力します。
まあ日本の方ならアカウント名見れば分かると思うので、シンプルにいきます。

あとはおすすめされる通知設定やフォローを全て拒否して完了です。

「Twitter Developers」でアプリケーションの作成

Twitter連携用のアプリケーションを作成します。

https://dev.twitter.com/

サイト上の「My Apps」をクリックし、次画面の右上からログインしましょう。

shot2ss20161223231237770

アプリ名、アプリの詳細、サイトURLを入力します。
割と適当でも大丈夫です。
サイトを持っていない場合は適当なURL(example.comとか)でも良いらしいです。

作成時に電話番号認証が行われていない場合、エラーとなって先に進めません。
しかも電話番号はアカウント間で共有することが出来ないそうです。
面倒ですが、メインアカウントの電話番号で一時的に認証してしまいました。

完了後にアプリケーションの詳細画面が表示されるので、「Application Settings」のあたりを撮っておきます。
上タブの「Keys and Access Tokens」の情報と、画面下の「Create My Access Token」クリック後の情報もメモしておきます。

実行用PHPファイルの作成と自動実行設定

GitHub上で公開されている「TwitterOauth」というPHPで作るのが一般的らしいです。
ほぼ他の参考サイトのコピペです。

<?php
require_once("twitteroauth-master/autoload.php");
require_once('twitteroauth-master/src/TwitterOAuth.php');

use Abraham\TwitterOAuth\TwitterOAuth;

$customer_key = "customer_key";
$customer_secret = "customer_secret";
$access_token = "access_token";
$access_token_secret = "access_token_secret";

$filelist = file('rampa_serifu.txt');
if(shuffle($filelist) ){
    $message = $filelist[0];
}

$connection = new TwitterOAuth($customer_key, $customer_secret, $access_token, $access_token_secret);
$connection->post("statuses/update", array("status" => $message));
?>

file() でテキストファイルの中身を取得し、無作為に1行抽出しているようです。
TwitterOAuth のパスや各種キーは環境に応じて設定して下さい。

実際は後述の台詞集入力で手間取っているため、まだ動かしていません。
またツイート時に「どこで発言したか」も入れたいので、単純にテキストファイルから取ってくるだけではきついかなーとも思います。
行く行くは正規表現であれこれして出力する感じでしょうか。

台詞集の入力

あとはランパの台詞を読み込ませるだけなのですが・・・。
ランパが登場するのは「伝説のスタフィー たいけつ!ダイール海賊団」という作品になりますが、もう発売から10年近く経つゲーム故、台詞をまとめてあるサイトなんてどこにもありません。
もちろんROM解析する知識・技術もなければ、それを行うための機材もありません。

ただ、ソフト自体は今でもとってあります。DSもあります。
つまりこういうことです!

DSC_0277

素でゲームを最初からエンディングまでプレイし、ランパの発言を残すことなく記録していくしかないでしょう。
クリアデータがありますが、一度クリアしたステージのストーリーを再度見ることは出来ません。
また、マリオやカービィと違ってステージ中にも会話が入るので、そういった意味でもプレイしないと厳しいです。

必要なのは「ランパが落ちてくるところからダイール撃破まで」のステージ1~8で、9~10は(確か)一言も喋らないので除きます。
隠しステージの攻略が必要かどうかは微妙なところです。
(うる覚えですが、ドランパに関することでランパが何か喋った気がします)
その他サブ要素(今日のゲストとメモ)で幾つか台詞があった気がしますが、既存のクリアデータで回収できます。

・・・と思いきや、セーブ中にソフトの接触不良→再起動時にデータ真っ白というアクシデント!
サブ要素をメモった後だったのが幸いですが、(ほぼ)コンプリートしたデータが消し飛んでしまいました。
元々最初からやり直す予定だったので、新規で始めますが・・・。

DSC_0279

DSC_0280

何かステージがところどころおかしいです。
水中なのに地上扱いになっていたり、壁のブロックが消えていたりします。
使用している3DSのカートリッジスロットの認識が悪いので、本体を入れ替えてからプレイしないと危険な気がします・・・。
ちょっとやり方を考える必要がありそうです。

というトラブルがあったことを踏まえると、実際の完成はかなり先の話になってしまいそうです。
こういう「台詞bot」って割とよく見かけますが、どうやって作っているのでしょうか。
やはり気合で入力するしかないのでしょうか・・・。

一気にやるのは無理があるので、暇なときにやっていくようにします。
ゲーム的な難易度は低めなので、「詰まる」という意味での障害はないと思われます。

まとめ

というわけで、Twitterでランパの台詞botを「形だけ」作ってみました!
botだけなら自作でも簡単に作れてしまうようです。
むしろゲーム内のランパの台詞をかき集める作業の方が大変かもしれません・・・。
気が向いたらときに少しずつ作っていくしかなさそうです。

余談

海外では「starfy wiki」というシリーズ全体のwikiがあります。
スタフィーシリーズは5作目まで海外で発売されていなかったにも関わらずです。
過去シリーズはおろか、過去に発売されたグッズやROM内の没データまで、内容量も凄まじいです。
英語のサイトですが、スタフィー好きな方は読んでみると面白いかもしれません。

http://starfywiki.org/wiki/Main_Page

【Unity】横スクロールアクションのカメラワーク制御について

というわけで、今更ながらあけましておめでとうございます!
今年も「ゴマちゃんフロンティア」をよろしくお願いします!

例年であれば「あけおめ」専用の記事を書いているのですが、毎年振り返ってもロクなこと語っていないので、今年はやめておこうと思います。
「ゲーム開発の目標!」なんて守れたことはほとんどないので(ぁ

shot2ss20170106203043668

新年1発目は「横スクロールアクションにおけるカメラワークの制御」になります!
ゲームにおいてカメラワークは超重要な要素ですが、自分なりに作った仕組みはどれも欠陥品で、使い物になりませんでした。
他サイト様を参考にしつつ、やっとそれなりのものが出来たのでメモしておきます。

カメラの描画範囲に応じた制御

カメラワークを制御する目的の1つに、「カメラの描画範囲がステージの範囲を超えないこと」があります。
・・・何を言っているかよく分からないと思うので、以下のgifアニメーションをご参照下さい。

20170106_01

20170106_02

上はカメラとプレイヤー位置(用のオブジェクト)を親子関係でくっ付けているだけなので、カメラ移動は完全にプレイヤーの移動に依存します。例えば、「ステージが横スクロールのみで縦スクロールさせたくない!」という場合でも、プレイヤーがジャンプすればカメラが上下に動いてしまう状態です。
また、カメラから見たプレイヤーの位置は常に画面の中央です。ステージ端まで行ってもプレイヤーが中央にいるため、画面の半分近くを壁(ステージ外の空間)が占領してしまいます。

下は本記事の趣旨である「カメラ移動範囲」の制御を取り入れたものです。
こちらもカメラとプレイヤー位置を親子関係でくっ付けていますが、ステージの各セクションごとに決められた範囲(以下セクション範囲と呼称します)に応じて、カメラの描画範囲を超えないようにカメラ移動を制限します。
gifアニメーションでは少し分かりにくいですが、これならステージの範囲を超えればカメラが上下せず、画面端まで行けばカメラ移動も止まります。

実装方法ですが、基本的には下記サイトの方法で設定しています。
非常に参考になりましたorz
http://pokelabo.co.jp/creative-blog/?p=340

shot2ss20170106202106038

オブジェクトとしては、CameraController と Section、SectionArea があります。
CameraController の下に mainCamera が存在し、Section はステージに配置したオブジェクトの親にしています。
わざわざ CameraController の子にしているのは、プレイヤー位置から見た相対的なカメラ位置・回転を変更できるようにしたかったためです。
SectionArea はセクションの範囲の基準点となる空オブジェクトです。
それぞれのオブジェクトにスクリプトを設定します。

【CameraController】

using UnityEngine;
using System.Collections;

/// <summary>
/// ゲーム画面上のメインカメラの制御を行うクラス
/// </summary>

public class CameraController : MonoBehaviour {

    // カメラの幅と高さ
    private float cameraRangeWidth, cameraRangeHeight;

    // 各オブジェクトとコンポーネント
    private GameObject mainCamera;
    private GameObject player;

    // セクション範囲定義用
    private Rect SectionRect;
    private Vector3 top_left, bottom_left, top_right, bottom_right;

    void Start() {
        // パラメータに値を設定
        player = GameObject.FindGameObjectWithTag("Player");
        mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
    }

    void Update() {
        // プレイヤーキャラの位置に追従させる
        transform.position = player.transform.position;

        Vector3 newPosition;

        // カメラ描画範囲の上下左右を取得
        float distance = Vector3.Distance(mainCamera.transform.position, player.transform.position);
        bottom_left = mainCamera.GetComponent<Camera>().ViewportToWorldPoint(new Vector3(0, 0, distance));
        top_right = mainCamera.GetComponent<Camera>().ViewportToWorldPoint(new Vector3(1, 1, distance));
        top_left = new Vector3(bottom_left.x, top_right.y, bottom_left.z);
        bottom_right = new Vector3(top_right.x, bottom_left.y, top_right.z);

        cameraRangeWidth = Vector3.Distance(bottom_left, bottom_right);
        cameraRangeHeight = Vector3.Distance(bottom_left, top_left);

        // カメラ位置をセクション範囲内に収める
        float newX = Mathf.Clamp(newPosition.x, SectionRect.xMin + cameraRangeWidth/2, SectionRect.xMax-cameraRangeWidth/2);
        float newY = Mathf.Clamp(newPosition.y, SectionRect.yMin + cameraRangeHeight/2, SectionRect.yMax - cameraRangeHeight/2);

        transform.position = new Vector3(newX, newY, newPosition.z);
    }

    void OnDrawGizmos()
    {
        // カメラ描画範囲を表示
        Gizmos.color = Color.green;
        Gizmos.DrawLine(bottom_left, top_left);
        Gizmos.DrawLine(top_left, top_right);
        Gizmos.DrawLine(top_right, bottom_right);
        Gizmos.DrawLine(bottom_right, bottom_left);
    }
}

【SectionController】

using UnityEngine;
using System.Collections;

public class SectionController : MonoBehaviour {

    // セクションのカメラ範囲制御
    public Transform SectionArea;
    public float rect_width, rect_height, collider_depth;

    private Rect SectionRect;

    // マネージャ
    private GlobalManager globalManager;

    void Start () {
        // マネージャ取得
        globalManager = GlobalManager.getInstance();

        // セクション範囲定義
        SectionRect = new Rect(SectionArea.position.x, SectionArea.position.y, rect_width, rect_height);

        // セクション判定用オブジェクトに範囲を設定
        SectionArea.GetComponent<SectionArea>().setSectionRect(SectionRect);

        // CameraControllerにセクション範囲を渡すための判定定義
        SectionArea.transform.position = new Vector3(SectionRect.center.x, SectionRect.center.y, transform.position.z);
        BoxCollider boxCollider = SectionArea.GetComponent<BoxCollider>();
        boxCollider.size = new Vector3(SectionRect.width, SectionRect.height, collider_depth);
    }

    void OnDrawGizmos()
    {
        if (globalManager) {
            // セクション範囲を描画
            float base_depth = globalManager.stageManager.baseDepth;

            Vector3 lower_left = new Vector3 (SectionRect.xMin, SectionRect.yMax, base_depth);
            Vector3 upper_left = new Vector3 (SectionRect.xMin, SectionRect.yMin, base_depth);
            Vector3 lower_right = new Vector3 (SectionRect.xMax, SectionRect.yMax, base_depth);
            Vector3 upper_right = new Vector3 (SectionRect.xMax, SectionRect.yMin, base_depth);

            Gizmos.color = Color.red;
            Gizmos.DrawLine(lower_left, upper_left);
            Gizmos.DrawLine(upper_left, upper_right);
            Gizmos.DrawLine(upper_right, lower_right);
            Gizmos.DrawLine(lower_right, lower_left);
        }
    }
}

globalManager など、多少触れていない要素が含まれていますが、ただ他のコンポーネントから値を取っているだけなので適当に流して下さい。

CameraController の Mathf.Clamp() がミソのようです。
これで CameraController のX軸Y軸がセクション範囲から出ないようにコントロールしています。
計算式の意味は参考サイトの持ってきただけなので、自分でもよく分かっていませんが・・・。

shot2ss20170106202358374

SectionController の boxCollider は後述する「セクション移動時のエリア再設定」用です。
セクション範囲の大きさをインスペクターから rect_width, rect_height, collider_depth に設定します。
その際、SectionArea の位置を左下とした値に設定する必要があります。
また、こちらは動的に動かしたりはしないので、本当に定義するだけです。

20170106_04

この状態で実行しシーンビューに切り替えると、セクション範囲が赤枠で、カメラ範囲が緑枠で表示されます。
上のgifアニメーションは mainCamera を選択している状態です。緑枠が赤枠を超えた場合、mainCamera の位置が動いていないのが分かると思います。

セクション移動時のエリア再設定

実際のゲームで考えてみると、ステージ全体を通して1セクションで出来ていることはなかなかないです。
「右に進んだら上に行って、今度は下ってまた右に~」なんてことが多いのではないでしょうか。
途中で特殊なエリア (中ボス部屋とか) を挟んだりする場合もあります。

なので、セクションを複数定義した際に CameraController のセクション範囲を再定義できるようにしておきます。
また、各セクションの子オブジェクトにセクション範囲を定義したオブジェクトを作ります。
付けるスクリプトは以下になります。

using UnityEngine;
using System.Collections;

public class SectionArea : MonoBehaviour {

    private Rect SectionRect;
    private CameraController cameraController;

    /// <summary>
    /// セクション範囲を設定する
    /// </summary>
    /// <param name="rect"></param>
    public void setSectionRect(Rect rect) {
        this.SectionRect = rect;
        cameraController = GameObject.FindGameObjectWithTag("CameraController").GetComponent<CameraController>();
    }

    void OnTriggerEnter(Collider c) {
        if (TagUtility.getParentTagName(c.gameObject.tag) == "Player") {
            cameraController.setSectionRect(SectionRect);
        }
    }
}

OnTriggerEnter() 時に CameraController にセクション範囲を渡すので、胴スクリプトにセッタを作ります。
至って普通のセッタです。

public void setSectionRect(Rect SectionRect) {
    this.SectionRect = SectionRect;
}

あとはプレイヤーがセクション範囲に触れれば勝手にセットされます。

セクション移動時のカメラワーク

一通りの実装はできましたが、セクションからセクションへ移動した際のカメラワークが微妙で、カメラが一瞬で移動するので視覚的にはよろしくありません。
こういった処理では iTween を使用するのがベターですが、「移動する先」が分からないと処理しようがないです。
現状でも一瞬で移動するのは、Update() の「セクション範囲にカメラを収める」ための処理で勝手に動いている感じです。

ということで、setSectionRect() 実行時に移動先の地点を割り出すための再計算を行い、 iTween による移動処理を加えます。
再計算といってもやっていることは Update() 内と変わりません。
ついでに移動用に関数として、moveCameraPosition() も作っておきます。

public void setSectionRect(Rect SectionRect) {
    // 移動後のカメラ位置取得のため、カメラ移動範囲の再計算
    float newX = Mathf.Clamp(mainCamera.transform.position.x, SectionRect.xMin + cameraRangeWidth/2, SectionRect.xMax-cameraRangeWidth/2);
    float newY = Mathf.Clamp(mainCamera.transform.position.y, SectionRect.yMin + cameraRangeHeight/2, SectionRect.yMax - cameraRangeHeight/2);
    Vector3 newPos = new Vector3(newX, newY, mainCamera.transform.position.z);

    moveCameraPosition(mainCamera.transform.position, newPos);
    this.SectionRect = SectionRect;
}

public void moveCameraPosition(Vector3 oldPos, Vector3 newPos, float time = 1f) {
    iTween.MoveTo(mainCamera, iTween.Hash(
        "position", newPos,
        "time", time
    ));
}

20170106_03

iTween.MoveTo() により、適用前よりもなめらかにカメラが移動してセクション再設定が行われます。
なかなか良い感じですね!

まとめ

そんなわけで、横スクロールアクションにおけるカメラワークについて考えてみました!
やっとまともなカメラワークが実現できたので一安心です。

ただ、実装が完全に横スクロール向けのカメラワークなので、3Dとのハイブリッドはほぼできなくなりました。
いっそ開き直ってゲームにすることを優先し、次作るゲームに繋げるというのもありかなーと思い始めていたりします。
なので気にせずやっていこうと思います!

【Unity】横スクロールアクション作成のためのオブジェクト移動制御

というわけで、年越しを前にして風邪が全然治らないりべるんです。
ブログも2週間近く放置していたので、年越し前に何か書いておこうと思います!

shot2ss20161230224759374

今回はUnityで横スクロールアクションを作る際の移動制御についてになります。
最近のゲームでありがちな、「3Dで作られているけど中身は2D」な感じです。
故にUnityの2D機能とは趣旨が異なるためご注意下さい。

こんな記事を書いた理由は他でもなく、自作ゲームを3Dで作ることに挫折しかけているためです。
ステージ作成が2Dの方がやりやすいのもありますが、カメラアングルの問題を上手く解決できません。
そのあたりのセオリーが分かるまで、ステージは2D移動をベースにして作ろうと思っています。

プレイヤー移動を2Dに限定

前に記事で紹介した、「CharacterMotor使用時の移動を2Dに限定する」でプレイヤー移動を2D方向に限定できます。

【Unity】CharacterMotor使用時のキャラクター移動を2Dに限定する

ただしこれだけでは完全ではなく、Unity側の物理演算の結果によってZ軸に移動する可能性があります。
適球状のオブジェクトを配置し、横から突っ込むと分かりやすいです。
上の対応は「プレイヤー側の入力で」Z軸に移動しないだけなので・・・。

スマートに解決する方法が分からなかったので、Update() 内で強引にZ軸を制御しました。
RigidBody があれば FreezePosition を指定することで解決しますが、CharacterController を使用している関係で使用できません。

void Update () {
    transform.position = new Vector3(transform.position.x, transform.position.y, globalManager.stageManager.baseDepth);
}

スタックオーバーフローには「Z軸を0にしろ(意訳)」的なものがあったりします。
が、自分はZ軸が0になるようにステージを作っていなかったので、真に受けて設定すると異次元にぶっ飛んでしまいました。

ということで、ステージ用のマネージャクラスに「現在ベースとなっているZ軸」を持たせることにしました。
ステージ進行に合わせて変化させることも可能で、その値でプレイヤーのZ軸を固定します。
マネージャクラスについては趣旨から外れる上に我流なので割愛しますが、 単に Player クラスのフィールドに持たせても問題ありません。
その場合、Start() 内で transform.position.z を元に初期化すればOKです。

20161230_01

ダッシュ時に挙動が怪しくなる時がありますが、概ね固定されるみたいです。

オブジェクト・敵キャラクター移動を2Dに限定

RigidBody の「FreezePosition」でZを指定してあげるだけでOKです。
あまりにも簡単すぎてプレイヤー制御の苦労が嘘のようです。

shot2ss20161230230000329

・・・本当にこれだけでOKなので、あまり書くことはないです!
「RigidBody万歳!」といったところでしょうか。

強いて言えば、敵キャラクターのロジックが3D用になっているので、それを修正します。
プレイヤーと同じように左右のみ瞬時で振り向くようにします。
Slerp() で補完していたのを LookRotation() に置き換えるだけでOKです。
その際、Y軸とZ軸を0に設定して振り向かせます。

void Update() {
    if (lookTarget != null) {
        Vector3 angle = lookTarget.position - transform.position;
        angle.y = 0;
        angle.z = 0;
        transform.rotation = Quaternion.LookRotation(angle);
    }
}

lookTarget にはプレイヤーの Transform を入れます。
実際は別オブジェクトに判定を持たせ、OnTriggerEnter() でセットしています。
(これまた趣旨と外れるので割愛します)

まとめ

かなりざっくりですが、横スクロールアクションにおける移動制御について考えてみました!
自分が作りたいゲームを想像すると、何かと2Dっぽい部分が多い気がするので、とりあえずこれで行ってみようと思います!

2016年もあと24時間ちょっとです。今年も早かったですね。
来年も「ゴマちゃんフロンティア」をよろしくお願いします!