2013年3月29日金曜日

TNet 1.65, 1.66


TNet バージョンアップ情報です:

http://www.tasharen.com/forum/index.php?topic=2650.0
March 20, 2013, 09:15:53 PM


1.6.5:
- 新機能: TNManager.channelID を追加し、現在のチャネルを取得できる関数を準備した
- 新機能: 各々のチャネルでカスタム文字列を特定できる機能を追加し、これによりチャネルに関する説明文をつけることが出来るようにした
- 新機能: 存在しない RFC 関数を実行しようとするとエラーメッセージが Unity で表示されるように機能を追加
- 修正: バージョンが一致しないときも保存ファイルがロードされてしまう不具合を修正。
- 修正: TcpChannel を Channel に名称変更(TCP とは関係がないため)。
- 修正: プレイヤーが接続状態でチャネルに入室しているときに TNManager.isInChannel の戻り値が true となるように修正。
- 修正:「 接続状態であればデータ送信を行う」ケースとなっていた処理の多くを、より正しい対応である「チャネルに入室していればデータ送信を行う」処理に修正。
- 修正: その他、様々なマイナー修正

1.6.6:
- 新機能: ロビーで TCP および UDP のいずれかを選択して使用できる機能を追加できるように サーバアプリを再構築した。
- 修正: Star Dots での開発から学んだ様々な微修正や修正を組み込んだ。
-----------

TNetも日々改善が進んでいるぜ!しかし、Unity New GUI を開発する時間は一体いつ確保できているんだ??

2013年3月22日金曜日

NGUI 2.5.0

NGUIバージョンアップ情報です:

※Unity 3.5.4以前を使用している方はアップグレードしてはいけません!!その場合はAsset Storeからうっかりダウンロードしないようにも注意が必要

http://www.tasharen.com/forum/index.php?topic=11.25
March 20, 2013, 03:34:35 PM

2.5.0
- 削除: Unity 3.5.4 より前のサポートを終了。3.5.4 より前のバージョンを使用している場合はアップデートしないこと!
- 廃止: Sliced, tiled, および filled スプライトを廃止予定(depreceated)
- 新機能: 通常のスプライトで、スプライト描画方法に関するオプションを新たに機能追加。
- 新機能: NGUI ウィジェットに 視覚できる配置用のハンドルを機能追加。
- 新機能: ウィジェットの追加により、ない場合は UI 階層を自動作成する機能を追加。
- 新機能: NGUI メニューをデザインし直し、新たなオプションおよびショートカットキーで再設計。
- 修正: ウィジェット選択ボックスが padding を正しく考慮しない不具合を修正。
- 修正: ピボットの変更によりウィジェットを視覚的に移動する不具合を修正。
- 修正: フォント シンボル がオフセットのために inner rect の代わりに padding を使用するように修正。
- 修正: フォント シンボル をゲーム内で使用する際に、事前にエディタで使用されている必要がある不具合を修正。
- 修正: Tween の初期化/スタート時の方法を修正。
- 修正: 下位互換のため、UISlider.fullSize プロパティを再度追加。
- 修正: Unity 4.1 関連の修正。
- 修正: 様々な軽微な修正やマイナーな変更。
-------

過去のバージョンとの下位互換も少しづつながら手を入れ始めてきた様子。別記事で取り上げたとおり、色々と悩みながら開発を進めているのは事実のようだ。

関連記事

NGUI作者、新バージョンに大いに悩む
http://gamesonytablet.blogspot.com/2013/03/ngui.html

2013年3月18日月曜日

NGUI作者、新バージョンに大いに悩む

なにやらNGUI作者が新バージョンをどうするか悩んでるようだ。Unityのアセットストア売り上げNo.1でUnity Technologies社にも入社して何を悩むのかとおもうかもしれないが、大真面目に新バージョンについてどうするか、ということのようだ。

NGUI掲示板での告白がGoogle翻訳ではうまく伝わりきらないんで、意訳と口語体バリバリの"超訳"でどうぞ!

------------
http://www.tasharen.com/forum/index.php?topic=3484.0


今日はめずらしく真剣にみんなの意見を聞きたいことがあって投稿してみることにしたよ。是非意見を聞かせてくれよ!

下位互換ってマジ大変・・・


実はNGUIの開発をやってきて、前々から下位互換で身動きがとれなくなってたんだ。新しい解決策を見つけるにつれて(たとえばイベントのハンドリングを例にすると分かり良いのだけど)、過去プロジェクトとの下位互換を考えると NGUI をどうしても変更できない場面があった。だからこそ NGUI はコア部分は一年前と対して変わってないし、その証拠として NGUI フリー版でも現行バージョンの NGUI でされたプロジェクトは開けるようにしてきたんだ。

いやさ、考えてみると Unity も同じ悩みあんだろうね。いっつも下位互換を維持して修正しないのと、何か変更して良く修正すんのと、両方の質問があるときは勝つのがいつも下位互換だったんだよね、チクショー。

ま、知っているとおもうけどね、Unity でのオイラの仕事は "次世代" UI システムを作ることなわけさ、NGUI の後継になるようなモンをね。これを最高級の仕事にするためにはね、時には妥協も必要ってわけさ。イベントシステムを変えていきたいのは良い例でさ(これは Unity の将来バージョンで個別に同梱されるとおもうけどさ)。

イベントとかスプライトとかグッと改善したいんだけど・・・


もっと具体例を言わせてもらうとね、NGUI にはちいと知られた OnPress(bool isPressed) イベントってもんがあるわけさ。新システムは  OnPressEvent() および OnReleaseEvent() を代わりに送信すんだけど、この方が Playmaker とか他のツールと相性が良いわけでね。新しいイベントシステムではね、OnDragEnterEvent / OnDragExitEvent なんか追加しちゃったりするつもりでね、これでボタンをタッチした後にスライドして別のとこに行ってもバッチリ対応できるように考えてるわけさ。このイベントシステムは Unity ネイティブなんでね(C++ 側でビルドしてるから)、自然とレスポンスも大分と早くなるわけで。OnGUI ボタンともうまく連携するわけで、デバッグでまだ使い続けたい人にも役立つはずさ。

他に変更する箇所の例をあげるとなー、UISpriteとか UIFilledSpriteとか UISlicedSprite とか UITiledSprite(種類多かったなー)をついに一個のスプライトでまとめて置き換えるつもりなんでさ。コイツが出来れば、コンテンツの描画とか、デプスと Z バッファの優先順位、イベント通知(OnSubmit とか値が変わった時、とかね)の完全なリプレースとか、その他にも色々選択肢が広がるわけさ。

聞いてりゃ良くなることばっかりと分かるだろうけどさ、改善するともう下位互換はボロボロになるわけで。全部変更して NGUI の過去プロジェクトと下位互換を維持するなんて無理!!って言ってももう10万ユーザもいること考えると、見捨てることになるとトホホなんだよね。

配信方法もネックでさ・・・


まだとっておきのこと言ってなかったな。大幅変更して NGUI 3.0 にしたとして、アセットストアで既存バージョンをただ置き換えちゃうのはイケテない。何でかって、必要なときに古いバージョンをダウンロードする方法ないんだし、それを買ってくれたみんなに強制するなんざオイラ自身も許せないわけさ。あと考えられる方法で #ifdef 使って新しいシステムと切り分けるのもおバカなアプローチで、コードが倍になるしグチャグチャになっちゃう。これもプライドが許さんな。フリー版でリリースしてよ!って声あるかもしんないけど、それは勘弁ってだけじゃなくて、過去に購入したかどうかチェックするだけでも気が狂うような作業になるわけさ。10万ユーザ分で切り分ける作業はチョット時間ないッス。

長ったらしくてスマンだけど、別のプロダクトとして販売するから皆にそれで選んでもらいたいってことなんだよね。

頼む、意見をくれ!(;´Д`)


でね、オイラの質問が出てくるわけさ。下位互換とオサラバして NGUI 新バージョンを作成してそこから今年後半ぐらいに新しい UI システムにスムーズに移行できるようにしたら、それでも使ってくれる?あと、もうひとつ重要なことなんだけどオイラの立場にたってもらって、それは時間を費やすべきことだとおもってもらえるだけのプロダクトだと思うかな?

あの・・・、実はオイラの結論は「いや、作るべきじゃない」に傾いてきているんだ。まあ、予想は皆としてはフリー版で対応してくれよっていう声が強いんじゃないかな、と思ってることが大きいんだけどね。是非みんなの意見も聞きたいわけさ。

この究極の選択、皆はどう思う?
------------

ちいと口語体過ぎたかな・・・でも言っていることは合っているぜ?!

意見ある人はどんな英語でも良いから、フォーラムで回答してあげようぜ!

2013年3月17日日曜日

Missing Scriptsエラーの解消(後編)

前回記事に続いて、Missing Scripts エラーをエディタで自動的に解消するスクリプトの解説だ。

関連記事


Missing Scriptsエラーの解消(前編)
http://gamesonytablet.blogspot.com/2013/03/missing-scripts.html

ではいつもどおり、Unity Gemsからの翻訳をどうぞ!

----------
http://unitygems.com/lateral1/
November 14, 2012

データを利用可能なスクリプトのフィールドと比較


ここからが難所だ。振り返ると、Missing Script が出たゲームオブジェクトで GetComponent を行うと null が戻る。とすると null しか戻らないものに対してどうやってプロパティの一覧を取得するのか、ということが課題となる。

さらに問題なのが、Unity 自身が C++ バックエンドの中でスクリプトの取得およびエディタ割り当てを内部で実行していて、Editor クラスでも手が届かないのでアクセス権もないことだ。

これで私はかなり悩まされた(実は始める前からこの辺りは相当問題になるだろうことは理解していたが、まあ話をわかり易くするため、その辺りの前後関係は省略する)。

事実関係を整理すると:

  • Missing Script で GetComponent を行うと null が戻る
  • シリアライズされたデータは、それに対する参照がないとアクセスする方法はない
  • インスペクタではどのスクリプトか分からなくともプロパティの一覧を表示する
  • 明白なことは、最後のポイントがこの問題の鍵だということだ。C# で利用可能なものは、そのデータストリームに対するアクセスが必要だ。さらに、それはインスペクタで表示される Editor でなければならない

このため、やるべきことはそのシステムに適切に統合して、Editor の中で Missing Object  を検知できるようにすべきだ。つまり MonoBehaviour 自身のカスタム エディタを記述してみよう。

必然的に、これを行うためにはデータストリームをインスペクタ上の serializedObject で受け渡しをしないといけない。これができれば一歩前に進める。次にやるべきことはMonoBehaviourが Missing Script かどうかの判定だ。このために取得したオブジェクトの SerializedProperties をみる必要がある。

さて、次はデバッグの出番だ。データストリームには何が含まれているか、確認が必要だ。

SerializedObjectの値を見ていくには GetIterator() をコールする。すると SerializedProperty が戻るので、さらに Next または NextVisible をコールして、値の間を遷移していく。これら二つのコールは共に boolean 型の引数を指定してデータのドリルダウンが出来る。最初のコールでは true でないといけないが、逆にその後のコールでは false でないといけない。
var property = serializedObject.GetIterator():
var first = true;
while(property.NextVisible(first))
{
     first = false;
     Debug.Log(property.name);
}
シリアライズ (インスペクタで表示) されたデータの各メンバーに対して Debug.Log を使用することで、現在 null 格納されている m_Script 変数に格納されていることが分かる。そうすると、ストリームの残りはデフォルトのインスペクタが描画しているパブリックのプロパティを保有している。これで解決策は完了で、データストリームはアクセスが可能だ。

後やるべきことは、これらの変数を候補となるスクリプトで整理して、全部が m_Script とマッチングするかチェックして、それさえ出来れば変数としてセット出来る。
// シリアライズ プロパティのスクリプトのコピーを作成
// して、後に参照
var script = iterator.Copy();
// 全てのスクリプトの一覧を取得
var candidates = scripts.ToList();
// 残りの引数をチェック
// して、マッチングするものがあるか確認
while(iterator.NextVisible(false) && candidates.Count>0)
{
    // 候補のスクリプトを現在のプロパティを保有する
    // サブセットに設定
    candidates = candidates.Where(c=>c.properties.ContainsKey(iterator.name)).ToList();
}
// もし候補が一つだけの場合は
// それを使用
if(candidates.Count==1)
{
    // スクリプトの参照をセット
    script.objectReferenceValue = candidates[0].script;
    // データストリームを更新
    serializedObject.ApplyModifiedProperties();
    serializedObject.UpdateIfDirtyOrScript();
}
// もし複数マッチングした場合は
// ユーザに選択肢を提供
else if(candidates.Count > 0)
{
    foreach(var candidate in candidates)
    {
        if(GUILayout.Button("Use " + candidate.script.name))
        {
            // スクリプト設定を編集
            script.objectReferenceValue = candidate.script;

            serializedObject.ApplyModifiedProperties();
            serializedObject.UpdateIfDirtyOrScript();
        }
    }
}
// それ以外はエラーメッセージ表示
else
{

    GUILayout.Label("> 適切なスクリプトは見つかりませんでした");
}

既存の壊れたオブジェクトを修正して正しいスクリプトを参照させる


前述のスクリプトでこれを確認することが出来る。m_Script の SerializeProperty である、objectReferenceValue を新しいスクリプトの値と同じにして、親の SerializeObject で ApplyModifiedProperties() をコールする。

Bodging に関するテクニック


さて賢明な方はすでにお気付きと思うが、欠点として、スクリプトを一つづつしか修正出来ない。どうやって全てのスクリプトをチェックして、一つづつ選択せずに直せるのだろうか。

その解決策が Bodging だ。二段論法でいうと:


  • 実際のデータ上でアクセスできる場所はインスペクタ
  • インスペクタはエディタ上で選択しないと修正できない


このためエディタをうまく騙して Missing Objects を各々選択させて、前述のコード実行を待機して、次のスクリプトに進む処理が必要だ。

これは Update 関数で工夫を凝らす必要があり、インスペクタ上でコードを追加して、どのコードが作業完了か認識させる必要がある。
        if(GUILayout.Button("Fix Now", GUILayout.Width(80)))
{
    FixMissingScripts.tried = false;
    EditorPrefs.SetBool("Fix", true);
    processList.AddRange(
        Resources.FindObjectsOfTypeAll(typeof(GameObject)).Cast().Where(c=>c.GetComponents().Any(o=>o==null))
        );
}

fix ボタンをクリックすると壊れたスクリプトは processList に移動され、カスタムエディタの Update 関数で処理される。ここではインスペクタのコードで作業済みの static 変数に false を渡していることに注目して欲しい。

次に Update で行うことは
if(!trying)
{
    if(processList.Count > 0)
    {
        FixMissingScripts.tried = false;
        var first = processList[0];
        FixMissingScripts.tryThisObject = first;
        processList.RemoveAt(0);
        Selection.activeObject = first;
        if(processList.Count==0)
        {
            nextTime = 10;
        }
        Repaint();
        trying = true;

    }
}
if(trying && FixMissingScripts.tried)
    trying = false;
何か作業中かチェックして、そうでなければ次をキューに入れて、完了時にインスペクタに作業済みの変数を更新するように指示する。

もし作業済みならば、現在のスクリプトの処理が完了したか確認をして、完了していれば次のアイテムへと進む。

結論


Bodging は有効に活用できる。これは時に水平思考の一種ぎ必要だ。どうすれば特定のデータストリームを取得出来るだろうか。しばらく画面を眺めたうえでアハ体験に遭遇するようなことご必要で、インスペクタで取得出来るならばインスペクタ自身になるためにはどうすれば良いだろうか。残りは残念ながら Bodging により、最初の回避策を上手に活用して 100 個長のスクリプトでも正しい動作をさせる必要がある。

このプログラムは次のリンク先からダウンロード出来る。

プログラムをダウンロード


メニューから Window > Fix Missing Scripts とすることで修正用のウィンドウが開く。
--------

やったぜ!これでMissing Scriptsの解消が出来る!と安心するのは早計。

実際使ってみた感想としては:

  • 進捗状況が分からない
  • Missing がスクリプト以外の場合はみつけてくれない

など課題はある。考えようによっては上記スクリプトをカスタマイズすればそのあたりは解消できそうだし、ダメ元でチャレンジというぐらいのスタンスが良いのかも。

エディタプログラミングも勉強すると便利だぜ!

Missing Scriptsエラーの解消(前編)

Unity使っててプロジェクトをインポートするときなどに稀に Missing (Mono Script) などと表示されていることがある。

慣れている方なら「仕方ないな」といって手動で正しいスクリプト見つけて修正していくのだが、これをエディタ上で自動的に修正してくれるパッケージがあったので紹介したい。

試す前にバックアップをとっておくことをオススメするが、再現ビデオにあるとおりうまく行った場合は複数のスクリプトを使用する変数から自動判断して修正してくれる。
(もし変数が同じスクリプトが複数ある場合は、選択するダイアログがある)

プログラムをダウンロード

メニューから Window > Fix Missing Scripts とすることで修正用のウィンドウが開く。

まずまず便利といったところだろうか。Unity Gemsというサイトでこのプログラムのロジックまで説明してあるので紹介したい! (ちょっと長いので前編・後編に分ける)

----------
http://unitygems.com/lateral1/
November 14, 2012

次のケースでこのチュートリアルが有効活用出来ます:

  • Missing Scripts エラー を Unity で修正する方法を理解したい
  • 複雑な問題を切り分けるのにデバッグを使うアプローチを理解したい
  • SerializedObjects および SerializedProperties の理解を深める
  • Bodging テクニックで出来ることを知りたい


はじめに


 "Missing MonoBehaviour" エラーは Unity でプログラミング始めて間もなく遭遇するエラーだ。インポートやプロジェクトの移動を行った後、あるいはウッカリ変更した何かによって、突然コンソールに表示されてスクリプトの参照が壊れてしまう現象だ。スクリプトそのものをリストアしても参照は復旧しない。
これが一つや二つのオブジェクトなら良いが、これが100だったら本当に悪夢そのものだ。

私もこの問題に遭遇したが幸いに自分なりの解決方法を見つけた。自分が辿った手順が何か別の複雑な問題を解決するヒントになるだろうと思ったので共有することにした。こういう問題では世界中を敵にしたような絶望を感じがちだが糸口はあるのだ。

問題の解決方法


この問題の解決は四つのステップに分かれている。

  1. Missing Scripts の発生したゲームオブジェクトおよびプレハブの特定
  2. 全ての利用可能なスクリプトを特定
  3. データ (インスペクタで表示されているフィールド) を利用可能なスクリプトのフィールドと比較
  4. 既存の壊れたオブジェクトを修正して正しいスクリプトを参照させる

Missing Scripts の発生したゲームオブジェクトおよびプレハブの特定


さて最初のステップは容易だろう。Unityで現在のシーンやプロジェクトビューにある全てのゲームオブジェクトは一覧に出来る。良くある方法で、FindMissingScripts スクリプトで全てのオブジェクトを選択する必要がある方法は不便なのでここでは別の方法を考える。

全てのオブジェクトを見つけるには Resources.FindAllObjectsOfType を使用すれば良いだけだ。これでリソースを見つけられるだけでなく、それに加えてUnity 独自のものも、全て見つけられるといって良い。まずは全てのゲームオブジェクトを見つけて、それからそれらの全てのコンポーネントを見つけて Null でないか見つけることになる。

Linq では魔法のように一行で記述することも出来るが、可読性のために分割してみる:
brokenList =
    // 全てのゲームオブジェクトを見つける
    Resources.FindObjectsOfTypeAll(typeof(GameObject))
        // FindObjectsOfTypeAll は Object[] を戻す
        // このため GameObject enumeration ひ変更
        .Cast()
        // 各々のオブジェクトをチェック
        .Where(
            // 全てのゲームオブジェクトを取得
            c=>c.GetComponents()
                // null を戻すものごあるか確認
                .Any(o=>o==null)
        )
        // これをリストに型変換
        .ToList();
これでプロジェクトやシーンでの有無にかかわらず、壊れてるスクリプトを見つけることが出来る。

全ての利用可能なスクリプトを特定


これも同じような方法で対処出来る容易なものだ。MonoScript 型のオブジェクトを全て見つけるだけだ。ここでは条件がもう一つだけある。次のステップではインスペクタで表示されているプロパティにもとづいて Missing Scripts とマッチングを行う。すなわちスキャンしている全てのスクリプトのプロパティを知る必要があるということだ。ここは時間をじっくりかけて SerializeField をセットしてある public および private フィールドを取得したうえで 参照を速やかに行えるように Dictionary に格納する。
// 候補となる全てのスクリプト
static List scripts;
// スクリプトをスキャンしたかどうか

static bool _initialized = false;

// スキャンされたスクリプトを保持
class ScannedScript
{
    //全ての serialized プロパティ
    public Dictionary properties;
    // インスタンス id
    public int id;
    // スクリプト自身
    public MonoScript script;
}

// これらスキャン完了したスクリプトを初期化
void Initialize()
{
    if(_initialized)
        return;
    _initialized = true;

    ScanAll();

}

void ScanAll()
{
    // 全てのスクリプトを取得
    scripts = Resources.FindObjectsOfTypeAll(typeof(MonoScript))
        // MonoScript のコレクションとする
        .Cast()
        // システム スクリプトでないことをチェック
        .Where(c=>c.hideFlags == 0)
        // コンパイル完了であり、
        // クラスが取得出来ることをチェック
        .Where(c=>c.GetClass() != null)
        // 各々についてスキャンされたスクリプトを作成
        .Select(c=>new ScannedScript { id = c.GetInstanceID(),
            script = c,
            // プロパティは全て [SerializeField] をセットし
            // public および private とする必要がある
            properties = c.GetClass().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                .Where(p=>p.IsPublic || (!p.IsPublic && p.IsDefined(typeof(SerializeField), false)))
                .ToDictionary(p=>p.Name)
        })
        .ToList();
}
実行することで結果として、候補となるスクリプトが得られ、各々のスクリプトには シリアライズされた dictionary が得られる。

テクニカルかつ高度だがわかり易い。
------------

後編に続くぜ!

関連記事

Missing Scriptsエラーの解消(後編)
http://gamesonytablet.blogspot.com/2013/03/missing-scripts_17.html

2013年3月13日水曜日

NGUI 2.3.5, 2.3.6

NGUIバージョンアップ情報です:

http://www.tasharen.com/forum/index.php?topic=11.25
March 09, 2013, 05:08:31 AM

2.3.5:
- 新機能: フォント シンボルにオフセットを設定可能とする機能を追加、位置調整がより容易となる
- 修正: UISlider が、デリゲートをコールする前に "現在の" プロパティをセットするように修正
- 修正: 2.3.4により発生していたチェックボックスのアニメーションでの不具合を修正
- 修正: その他、軽微でマイナーな修正をいくつか対応

2.3.6
- 新機能: シンボルや顔文字(emoticon)を簡単に追加する機能を追加(フォント選択でやり方は確認可能)
- 新機能: UIPanel について、条件により判定する良くある間違いでの警告を行う機能を追加
- 新機能: ウィジェットおよびスプライトのインスペクタにいくつかの改善を追加
- 修正: フォントで存在しないのに "symbols" オプションをラベルに表示する必要があった不具合を修正
- 修正: ハードコードされていた画面の高さにもとづいたタッチの閾値を除外した
- 修正: スライダーが "フルサイズ" のプロパティを必要としていた不具合を修正
------

ちなみに、例のDynamic Fontの正式採用は次バージョンのUnity Pro版でリリースして、問題なければPro版以外でリリース予定とのことだ。


パッチでも良いが次こそ正式採用したリリース版を期待しているぜ!

関連記事


【朗報】NGUIにダイナミックフォント正式採用の方向性
http://gamesonytablet.blogspot.com/2013/02/ngui.html

2013年3月6日水曜日

AI開発&クォータニオン入門~8.コルーチンでアニメショーンを待機

関連記事

今シリーズの最終回。例のごとく、Unity Gemsからの翻訳をどうぞ!

----------------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012


コルーチンを使用してアニメショーンを待機する


すでに範囲内にあれば攻撃のコルーチンが開始される。

ここで実現したいことは通常の動作をポーズさせて、攻撃アニメショーンを再生してアニメショーンが半分進んだところで、攻撃がヒットしてダメージを適用することだ。次の方法でこれを実現する:

最初に - ルーチンをより便利なものとするため、Attack をコールしたときに敵をウェイクアップするようにする。これは敵同士で衝突した時も Attack をトリガー出来るので便利だ。
IEnumerator Attack(Transform victim)
{
    sleeping = false;
    _busy = true;
    target = victim;
    _attack.enabled = true;
    _attack.time = 0;
    _attack.weight = 1;
    //Wait for half way through the animation
    yield return StartCoroutine(WaitForAnimation(_attack, 0.5f));
    // 範囲内にまだいるかチェック
    if(victim && (victim.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared)
    {
        // ダメージを適用
        victim.SendMessage("TakeDamage", 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);
    }
    // アニメショーン終了を待機
    yield return StartCoroutine(WaitForAnimation(_attack, 1f));
    _attack.weight = 0;
    _busy = false;
}
 _busy = true とセットすることでルーチンを実行している間はUpdateを無効にする。 _attack はこちらで有効化して制御するアニメショーンであり、最初にセットして、重み付けの weight = 1 とする。これでアニメショーンが再生開始される。次が素晴らしいところだが、アニメショーンをモニタリングする新しいコルーチンを実行して、半分まで完了するのを待機する。ルーチンはこの後に見ていく。

アニメショーンが 50%  完了すると、攻撃した被害者 (victim) がすでに死亡してないかチェックして、まだ範囲内にいるか、さらにはそれらの条件が満たされてる場合はTakeDamage メッセージを渡してランダムな量のダメージが反映される。

次にアニメショーン終了を待機してから busy ステータスを外して、アニメショーンを無効化する。

このアニメショーンを待機するコルーチンについて見ていこう。
public static IEnumerator WaitForAnimation(AnimationState state, float ratio)
{
    state.wrapMode = WrapMode.ClampForever;
    state.enabled = true;
    state.speed = state.speed == 0 ? 1 : state.speed;
    while(state.normalizedTime < ratio-float.Epsilon)
    {
        yield return null;
    }
}
見ての通りかなり簡単だ。最初にアニメショーンがループやリセットしないようにする。セットしないと、終了のところを逃がすことがあるためだ。さらに enabled を true にセットし、speed がゼロとなることを確実にして、ルーチンが終わらないような不具合に陥らないようにあらかじめ手当てする。

次にループは .normalizedTime を待機して渡した値より大きいか等しくなるまで待機する。(浮動小数点の誤差を吸収する係数)

様々なステート


これで攻撃およびダメージのメッセージを送信出来るようになったため、メッセージをハンドリングする方法を見ていく。このデモでプレイヤーは無敵だが、敵同士はダメージを与えることができる。

敵キャラは TakeDamage を受け取ると次の動作を行う。
void TakeDamage(float amount)
{
    StopCoroutine("Attack");
    health -= amount;
    if(health < 0)
        StartCoroutine(Die());
    else
        StartCoroutine(Hit());
}
まずは現在の攻撃は全て取り止めて、ダメージを与えて、Hit または Die コルーチンのどちらかを開始する。
IEnumerator Die()
{
    _busy = true;
    _animation.Stop();
    yield return StartCoroutine(PlayAnimation(_die));
    Destroy(gameObject);
}

IEnumerator Hit()
{
    _busy = true;
    _animation.Stop();
    yield return StartCoroutine(PlayAnimation(_hit));
    _busy = false;
}
両方のコルーチンはアニメショーン終了まで再生し、Die はオブジェクトを破棄して、Hit はそれを再び有効化する。

さて、ようやくこれでこのチュートリアルは終わりまで一息だ。RotateToFaceTargetは初期に作成した回転スクリプトを置き換え、常にプレイヤーをみるのではなくターゲットの位置をみるようにするので、内容は良くみてほしい。この仕組みによって敵同士がターゲットとなりえる。他に注目すべきは WalkingAnimationだ:
using UnityEngine;
using System.Collections;

public class WalkingAnimation : MonoBehaviour {

    Transform _transform;
    Vector3 _lastPosition;
    AnimationState _walk;

    public float minimumDistance = 0.01f;

    // Use this for initialization
    void Start () {
        _transform = transform;
        _lastPosition = _transform.position;
        _walk = animation["walk"];
        _walk.layer = 2;
    }

    // Update is called once per frame
    void Update () {
        var moved = (_transform.position - _lastPosition).magnitude;
        _lastPosition = _transform.position;
        if(moved < minimumDistance)    
        {
            _walk.weight = 0;
        }
        else
        {
            _walk.weight = moved * 100;
            _walk.enabled = true;
            _walk.speed = 1;
        }
    }
}
このスクリプトは敵が移動する速度にもとづいて歩行アニメショーンをブレンドする。

最後に


このチュートリアルご役立ったならば何よりだ。コメントを残すのは自由で、他にもどんな機能を紹介したら良いかなどもコメントあると有難い。

Mike(whydoidoit)氏
-----------

今シリーズはいかがだっただろうか。筆者自身はUnity Gemsから色んなことを学べて感謝しているが、やはりプロジェクトサンプルをもとに自分のアイデアを加えて色々なテストをしてみるのがベストだとおもう。

今年はUnityは日本語情報も充実してきているぜ!

AI開発&クォータニオン入門~7.動作を加えてみる

関連記事

では例のごとく、Unity Gemsからの翻訳をどうぞ!


----------------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012

動作を加えてみる


さて面白みを加えるために敵キャラがプレイヤーを攻撃する動作を追加する。再びクォータニオンを使用して敵キャラがターゲットとする位置変化させ、プレイヤーの周りを違った形で回転させることで背後から回り込んで襲ってくるようなロジックに変えてみたい。いっぺんにそこまで出来ないので、まずは MovementStateMachine3 を見てみよう。面白みを足すために大幅にコード行数を足してみた:
using UnityEngine;
using System.Collections;
 
public class MovementStateMachine3 : MonoBehaviour {
 
    public bool sleeping = true;
    public Transform sleepingPrefab;
    public float attackDistance = 5;
    public float sleepDistance = 30;
    public float speed = 2;
    public float health = 20;
    public float maximumAttackEffectRange = 1f;
 
    Transform _transform;
    Transform _player;
    public Transform target;
 
    public EnemyMood _mood;
 
    CharacterController _controller;
    AnimationState _attack;
    AnimationState _die;
    AnimationState _hit;
    Animation _animation;
 
    float _attackDistanceSquared;
    float _sleepDistanceSquared;
    float _attackRotation;
    float _maximumAttackEffectRangeSquared;
    float _angleToTarget;
 
    bool _busy;
 
    // Use this for initialization
    IEnumerator Start () {
        _transform = transform;
        _player = Camera.main.transform;
 
        _mood = GetComponent();
 
        _attackDistanceSquared = attackDistance * attackDistance;
        _sleepDistanceSquared = sleepDistance * sleepDistance;
        _maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;
 
        _controller = GetComponent();
 
        _animation = animation;
        _attack = _animation["attack"];
        _hit = _animation["gothit"];
        _die = _animation["die"];
 
        _attack.layer = 5;
        _hit.layer = 5;
        _die.layer = 5;
 
        _controller.Move(new Vector3(0,-20,0));
 
        while(true)
        {
            yield return new WaitForSeconds(Random.value * 6f + 3);
            if(sleeping)    
            {
                var newPrefab = Instantiate(sleepingPrefab, _transform.position + Vector3.up * 3f, Quaternion.identity) as Transform;
                newPrefab.forward = Camera.main.transform.forward;
            }
        }
 
    }
 
    void Update()
    {
        // 何か別にコントロールされてるかチェック
        if(_busy)
            return;
 
        if(sleeping)
        {
            if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)
            {
                sleeping = false;
                target = _player;
                // 敵キャラが攻撃する位置
                _attackRotation = Random.Range(60,310);
            }
        }
        else
        {
            // ターゲットが死亡した場合は、スリープに戻る
            if(!target)
            {
                sleeping = true;
                return;
            }
 
            var difference = (target.position - _transform.position);
            difference.y /= 6;
            var distanceSquared = difference.sqrMagnitude;
 
            // 十分に遠い位置にいるか?
            if( distanceSquared > _sleepDistanceSquared)
            {
                sleeping = true;
            }
            // 攻撃範囲内にいるか?
            else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f)
            {
                StartCoroutine(Attack(target));
            }
            // それ以外の場合は移動
            else
            {
 
                // ターゲットの位置を決定
                var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
                var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
                basicMovement.y = 0;
                //Only move when facing
                _angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
                if( _angleToTarget < 70f)
                {
                    basicMovement.y = -20 * Time.deltaTime;
                    _controller.Move(basicMovement);
                }
            }
        }
    }
 
    void OnTriggerEnter(Collider hit) 
    {
        if(hit.transform == _transform)
            return;
 
        if(hit.transform == _player)
        {
            StartCoroutine(Attack(_player));
        }
        else
        {
            var rival = hit.transform.GetComponent();
            if(rival)
            {
                if(Random.value > _mood.mood/100)
                {
                    StartCoroutine("Attack",rival.transform);
                }
            }
        }
    }
 
    IEnumerator Attack(Transform victim)
    {
        sleeping = false;
        _busy = true;
        target = victim;
        _attack.enabled = true;
        _attack.time = 0;
        _attack.weight = 1;
        // アニメショーンが半分進むまで待つ
        yield return StartCoroutine(WaitForAnimation(_attack, 0.5f));
        // まだ範囲内にいるか確認
        if(victim && (victim.position - _transform.position).sqrMagnitude < _maximumAttackEffectRangeSquared)
        {
            // ダメージを適用
            victim.SendMessage("TakeDamage", 1 + Random.value * 5, SendMessageOptions.DontRequireReceiver);
        }
        // アニメショーン終了を待機
        yield return StartCoroutine(WaitForAnimation(_attack, 1f));
        _attack.weight = 0;
        _busy = false;
    }
 
    void TakeDamage(float amount)
    {
        StopCoroutine("Attack");
        health -= amount;
        if(health < 0)
            StartCoroutine(Die());
        else
            StartCoroutine(Hit());
    }
 
    IEnumerator Die()
    {
        _busy = true;
        _animation.Stop();
        yield return StartCoroutine(PlayAnimation(_die));
        Destroy(gameObject);
    }
 
    IEnumerator Hit()
    {
        _busy = true;
        _animation.Stop();
        yield return StartCoroutine(PlayAnimation(_hit));
        _busy = false;
    }
 
    public static IEnumerator WaitForAnimation(AnimationState state, float ratio)
    {
        state.wrapMode = WrapMode.ClampForever;
        state.enabled = true;
        state.speed = state.speed == 0 ? 1 : state.speed;
        while(state.normalizedTime < ratio-float.Epsilon)
        {
            yield return null;
        }
    }
 
    public static IEnumerator PlayAnimation(AnimationState state)
    {
        state.time = 0;
        state.weight = 1;
        state.speed = 1;
        state.enabled = true;
        var wait = WaitForAnimation(state, 1f);
        while(wait.MoveNext())
            yield return null;
        state.weight = 0;
 
    }
 
}
さあ、面白みを増した箇所それぞれ見ていこう。まずは Start() の変更箇所だ。
_mood = GetComponent();

 _attackDistanceSquared = attackDistance * attackDistance;
 _sleepDistanceSquared = sleepDistance * sleepDistance;
 _maximumAttackEffectRangeSquared = maximumAttackEffectRange * maximumAttackEffectRange;

 _controller = GetComponent();

 _animation = animation;
 _attack = _animation["attack"];
 _hit = _animation["gothit"];
 _die = _animation["die"];

 _attack.layer = 5;
 _hit.layer = 5;
 _die.layer = 5;
自明なことだが、パフォーマンス上の理由であちらこちらでキャッシングを意識して行っている。また距離はすべて2乗の値を使用している。これは敵キャラからの距離を使用するときの平方根計算を避けるためである。

2点間の距離はピタゴラスの定理を使用して平方根計算を伴う。平方根計算は高価な再帰処理を使用する。この計算負荷を回避するにはチェックしたい処理の2乗を使用すれば良い。

いくつかのアニメショーンを高い値のレイヤーに使用して、通常のアイドル状態のアニメショーンが上書きされるようにした。

Update() 関数はかなり膨大だが、始まりの部分は次のようにコーディングする:
        //何か別にコントロールするものがあるかチェック
if(_busy)
    return;
攻撃するときや、ヒットされるときの動作については、キャラクターを別にコントロールするための仕組みを検討する。これが開始されると _busy が true となりUpdateを無視することができる。

次に敵がスリープしているときのロジックが必要だ。
        if(sleeping)
{
    if((_transform.position - _player.position).sqrMagnitude < _attackDistanceSquared)
    {
        sleeping = false;
        target = _player;
        // 攻撃する時の敵の位置
        _attackRotation = Random.Range(60,310);
    }
}
敵がスリープしているとき、attackDistance (攻撃距離) の範囲内にいるかどうかをチェックし、範囲内ならばスリープを解除してプレイヤーにターゲットを切り替える。さらにプレイヤーの周囲の特定の回転を割り当ててる。(敵キャラは巧妙に仕掛けてきて横や背後から迫ってくる。) 現時点ではただ変数として格納するだけで使用方法はこの後に見て行く。敵キャラがスリープ状態でないときの動作は次の通りとなる:
//ターゲットが死亡した場合はスリープに戻る
if(!target)
{
    sleeping = true;
    return;
}

var difference = (target.position - _transform.position);
difference.y /= 6;
var distanceSquared = difference.sqrMagnitude;

// 十分に遠い位置にいるか?
if( distanceSquared > _sleepDistanceSquared)
{
    sleeping = true;
}
// 攻撃範囲内にいるか?
else if( distanceSquared < _maximumAttackEffectRangeSquared && _angleToTarget < 40f)
{
    StartCoroutine(Attack(target));
}
// それ以外の場合は移動する
else
{

    // ターゲットの位置を決定
    var targetPosition = target.position + (Quaternion.AngleAxis(_attackRotation, Vector3.up) * target.forward * maximumAttackEffectRange * 0.8f);
    var basicMovement = (targetPosition - _transform.position).normalized * speed * Time.deltaTime;
    basicMovement.y = 0;
    // 見える角度にいる場合のみ移動
    _angleToTarget = Vector3.Angle(basicMovement, _transform.forward);
    if( _angleToTarget < 70f)
    {
        basicMovement.y = -20 * Time.deltaTime;
        _controller.Move(basicMovement);
    }
}
ターゲットが死亡したか、十分に離れた場合はスリープとした。それ以外の場合は攻撃範囲内にいるならば攻撃するためのコルーチンを実行する。スリープ状態でなく攻撃範囲内にいない場合はターゲットの方向に移動することにする。

これを実現するためにまず必要なのはプレイヤー位置の算出だ。これはターゲットの位置、ターゲットと決めたときの角度、攻撃範囲の距離、の組み合わせだ (距離の80%ぐらいとしたい) 。つまり、この角度にもとづいてクォータニオンを使用してターゲットの向きおよびベクトルを攻撃範囲の 80% となるようにスケールして修正する。これら全てで targetPosition をコールする。例えばもし選択した角度が180度だったとして攻撃範囲の距離が2だった場合、ターゲットの位置はカメラの真後か 1.6 となる。

次にプレイヤーの向きと行きたい位置の間の角度を算出する。もし、差分ごが大きすぎる場合はまだ動かないことする (回転スクリプトにより角度の問題を解決する)。これが問題なければ次はターゲットの位置に移動して重力を加え、敵キャラが地面から浮かないようにする。
---------
次回がいよいよ今シリーズ最終回!


関連記事


AI開発&クォータニオン入門~6.カメラの方向を向くようにPlaneを回転

関連記事

例のごとく、Unity Gemsからの翻訳をどうぞ!

----------------

カメラの方向を向くように Plane を回転させる


ここまで何とかしてテキストメッシュにカメラの方向を正しく向かせることには成功した。次は平面をどう扱うか考える番だ。プレイヤーに表示する 2D テクスチャを作成するのに Unity で作成した Plane は適している。画面の方向を常に向く 2D ビルボードを作成しよう。

ここで敵キャラに「ムード」があることにしよう。最初はある一定の機嫌の良さから始めて、時間の変化とともに機嫌が悪くなる。ゲームの中では色んな場面に応用して面白い要素に出来るはずだ。

プレイヤーは敵キャラを正面から見据えたときにこのムードが判別出来るようにしたい。敵キャラの頭上に機嫌を表すスマイルマークを表示して、色での工夫も重ねて機嫌がハッピーな時は緑色、怒りの時は赤色と、反映する。

当然、これにより必要なスクリプトは新たにいくつか必要となる。

最初に敵キャラを正面から見据える方法から考えて見よう。まずプレイヤーにスクリプトをアタッチして目の前のものに対して反応する処理を試す。
using UnityEngine;
using System.Collections;

public class LookTrigger : MonoBehaviour {

    Transform _transform;

    // Use this for initialization
    void Start () {
        _transform = transform;
    }

    // Update is called once per frame
    void Update () {
        RaycastHit hit;
        if(Physics.SphereCast(_transform.position + _transform.forward * 0.5f, 3f, _transform.forward, out hit, 400))
        {
            hit.collider.SendMessage("LookedAt", SendMessageOptions.DontRequireReceiver);
        }
    }
}
プレイヤーの正面の位置で SphereCast を実行してそれが何かと当たり判定があれば "LookedAt" されたとみなして、オブジェクトに対してその旨のメッセージ送信する。

ここで SphereCast を使用する理由は距離が離れたものについて多少のズレは許容する適度な正確さで扱ってくれるためである。

さて、ここまで簡単だったが次に敵キャラにムードを持たせる番だ。EnemyMood という新規のスクリプトの準備を進めていこう。
using UnityEngine;
using System.Collections;

public class EnemyMood : MonoBehaviour {

    public MoodIndicator moodIndicatorPrefab;
    public float _mood;

    Transform _transform;
    MoodIndicator _currentIndicator;

    void Start () {
        _transform = transform;
        mood = Random.Range(30,99);
    }

    void Update () {
        mood -= Time.deltaTime/2;
    }

    void LookedAt()
    {
        if(_currentIndicator)
            return;

        _currentIndicator = Instantiate(moodIndicatorPrefab, _transform.position + Vector3.up * 3.5f,
            Quaternion.identity) as MoodIndicator;

        _currentIndicator.enemy = this;
    }

}
初めに mood 変数を 30 と 99 の間の値で初期化する。次にこれを Update コールの中で 徐々にデクリメントさせる。

プレハブ変数を Plane に追加してムードを表示したうえ、プレハブに MoodIndicator スクリプトを与えるので、後ほど詳細に中身を検証していきたい。

EnemyMood は LookedAt メッセージを受け取り、すでに 既存の MoodIndicator があるかチェックし、あれば return で関数を終了する。

Unity は MoodIndicator が破棄されたときは自動的にこの変数の値として null/false を戻す。

もし既存の MoodIndicator がなかった場合、新たに作成して敵キャラの上に配置する。次に indicator に対して敵キャラのムードが現在どの状態であるかを通知する。

MoodIndicator は Plane の表示をハンドリングして適切な画像の表示を行う。
using UnityEngine;
using System.Collections;

public class MoodIndicator : MonoBehaviour {

    public Texture2D[] moodIndicators;
    public Color happyColor = Color.green;
    public Color angryColor = Color.red;
    public float fadeTime = 4f;
    public EnemyMood enemy;

    Transform _transform;
    Transform _cameraTransform;
    Material _material;
    Color _color;
    Quaternion _pointUpAtForward;

    void Start () {

        _pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

        _material = renderer.material;

        // 画像のセット
        _material.mainTexture = moodIndicators[
            Mathf.Clamp(
                Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
                0,
                moodIndicators.Length-1)
            ];

        // 色の計算
        var moodRatio = enemy.mood/100;
        _material.color = _color = new Color(
            angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
            angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
            angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
        );

        Update();
    }

    void Awake()
    {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    public void Update () {

        // カメラに対して Plane を表示
        _transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
            * _pointUpAtForward;

        // 画像のフェードアウト処理
        _color.a -= Time.deltaTime/fadeTime;
        _material.color = _color;
    }
}
重要なスクリプトの初登場なのでいくつかの部分に分けて見ていこう。
public Texture2D[] moodIndicators;
public Color happyColor = Color.green;
public Color angryColor = Color.red;
public float fadeTime = 4f;
public EnemyMood enemy;
パブリック変数は画像イメージの配列であり、ムードのインジケータとして使用して、悲しみ、喜び、等々を表す。サンプルプロジェクトには4種類含まれる。ハッピーな色と怒りの色があり、後は時間の経過とともにフェードアウトさせて最終的に全てが透明になる。最後に、どの敵キャラのものかの紐付け情報がある。これら全て Start 実行開始前までにセットされる。
void Start () {

    _pointUpAtForward = Quaternion.FromToRotation(Vector3.up, Vector3.forward);

    _material = renderer.material;

    //Set the graphic
    _material.mainTexture = moodIndicators[
        Mathf.Clamp(
            Mathf.RoundToInt( enemy.mood/(100/moodIndicators.Length)),
            0,
            moodIndicators.Length-1)
        ];

    // 色の計算
    var moodRatio = enemy.mood/100;
    _material.color = _color = new Color(
        angryColor.r * (1 - moodRatio) + happyColor.r * moodRatio,
        angryColor.g * (1 - moodRatio) + happyColor.g * moodRatio,
        angryColor.b * (1 - moodRatio) + happyColor.b * moodRatio
    );

    Update();
}
最初に行うべきことはキャッシュされたクォータニオンを作成して、その up ベクトルが forward ベクトルを向くようにする。これは Plane にカメラに向けるのと全く同じ方法の復習だ。

次にムードを表すテクスチャを選択するために、敵である怪物のムード(0 から 98 の間の数字)を一定の係数で割り算して、提供されているテクスチャの範囲に収まるようにして、必ずテクスチャが有効となるように clamp を行う。

次にスマイルマークの色を判断させるためにムードにもとづいて怒りおよびハッピーマークの一部を使用する。

最後に Update を呼び出して回転のコードが直ちに呼び出しされるようにする。

Update関数は次のようになる:
public void Update () {

        // Plane をカメラに向ける
        _transform.rotation = Quaternion.LookRotation(-_cameraTransform.forward, Vector3.up)
            * _pointUpAtForward;

        // 画像のフェードアウト
        _color.a -= Time.deltaTime/fadeTime;
        _material.color = _color;
    }

最初にカメラに向く通常のオブジェクトの回転を作成するために Quaternion.LookRotation 関数を使用してカメラの -transform.forward を渡して、これを、オブジェクトの up を forward を向けてキャッシュした回転と組み合わせる。これで Plane の設定は大丈夫だ。

回転の二つ目の部分は画像のアルファ値を事前に定めた時間でフェードアウトさせるだけの処理だ。

これで準備完了だ。実行して試すと敵キャラを画面の中央に持ってきた時にインジケータがポップアップする。敵キャラの機嫌もあっという間に怒りモードに変化する。
------
次に続くぜ!


関連記事


AI開発&クォータニオン入門~5.回転を組み合わせる


関連記事

では例のごとく、Unity Gemsからの翻訳をどうぞ!

----------------

回転を組み合わせる


ここまでの例で Zzzzzz の表示は退屈だったかもしれないが、ここでは一歩進んでクォータニオンの組み合わせをどうするか見て行くことにする。Zzzzzz 記号が上方向に流れながら徐々に回転する様子を見て行く。ここで意味していることは、カメラの方向は向いているけど、画面上をゆっくりとスピンする動作とする。

チュートリアル シーン7

この回転を得るために新しく SpinAroundForward スクリプトを作成する。
using UnityEngine;
using System.Collections;

public class SpinAroundForward : MonoBehaviour {

    Transform _transform;
    float _rotatingFactor;

    void Start()
    {
        _transform = transform;
        _rotatingFactor = 0.2f + (Random.value * 0.8f);
    }

    // Update is called once per frame
    void Update () {
        _transform.rotation = _transform.rotation * Quaternion.AngleAxis(Time.deltaTime * 20 * _rotatingFactor, Vector3.forward);
    }
}
このスクリプトは、Startの際に新規の回転の係数をランダム作成して、Updateの際にそれを適用する。

オブジェクトの現状の回転を Vector3.forward の周りの回転とどう組み合わせていくかの様子を見ることが出来る。* 演算子を用いて Zzzzzz の現在の回転と組み合わせることが出来る。

もしチュートリアル シーンでこのスクリプトを実行する場合、テキストは回転するけどカメラの方向を向いてないことが分かる。これには理由がある: ReverseCameraDirection スクリプトがオフになるからだ。もし Sleeping Text 3をクリックして ReverseCameraDirection を有効化すると今度は Zzzzzz ごカメラを向くが回転しないことが分かる。これは Transform ベクトルを直接に値変更するときにハマりやすいことだ。回転の組み合わせがグチャグチャになる。目的どおりにやりたい場合は別のアプローチが必要だ。

チュートリアル シーン8

シーン8 では ReverseCameraDirection にてクォータニオンを使用するように書き換えた AdjustForwardToPointToCamera と置き換える。
using UnityEngine;
using System.Collections;

public class AdjustForwardToPointToCamera : MonoBehaviour {

    Transform _transform;
    Transform _cameraTransform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    // Update is called once per frame
    void Update () {
        _transform.rotation = Quaternion.FromToRotation(_transform.forward, _cameraTransform.forward) * _transform.rotation;
    }
}
Quaternion.FromToRotationは調整が必要なベクトルがある場合に強力だ。これを使ってオブジェクトが配置されている物体の表面に合わせて回転させて、例えばそれが表面の法線と並ばせることが出来る。このケースでは Zzzzzz の transform.forward の回転を調整させてカメラのtransform.forwardと一致させることが出来る。

繰り返しになるが、メッシュは逆向きに反転していることを学習すべきだ。通常のオブジェクトの場合、オブジェクトの transform.forward を - transform.forward としないとカメラ方向を向かない。

作成したクォータニオンを前方向に回転させていたものに加えて、正しい角度と現在の回転 (SpinAroundForward スクリプトによりさらに変更されているもの) を組み合わせて完成だ。カメラが正しい方向を向いて、回転して Zzzzzz がついている (ようやくここまで来た!)。
-----
次に続くぜ!


関連記事

AI開発&クォータニオン入門~4.テキストをカメラの方向に向かせる

関連記事

例のごとく、Unity Gemsからの翻訳をどうぞ!

----------------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012


テキストをカメラの方向に向かせる


さあ、サンプルゲーム作成をもう一歩進めてみることにする。敵の動作はまだ単純すぎるので AI の要素を加えてみることにする。最初にすることはプレイヤーが近づくまでスリープ状態にすることだ。ここでは、スリープ状態を視覚的に分かるように頭上に Zzzzz とイビキをかいてる様子を描画してみることとする。

チュートリアルのシーン5

ここでは、まず初歩的なFSM (ステートマシン) を各々の敵に追加する。これは  MovementStateMachine に含まれる。
using UnityEngine;
using System.Collections;
 
public class MovementStateMachine : MonoBehaviour {
 
    public bool sleeping = true;
    public Transform sleepingPrefab;
 
    // Use this for initialization
    IEnumerator Start () {
        var t = transform;
        while(true)
        {
            yield return new WaitForSeconds(Random.value * 3f + 2);
            if(sleeping)    
            {
                Instantiate(sleepingPrefab, t.position + Vector3.up * 3f, t.rotation);
            }
        }
    }
}

これで、スリープ状態か判別する単純なステートマシンが得られた。ただし、これだけだとウェイクアップしたときに何も処理が行われない。さらにはウェイクアップさせる条件なども何も決めてない。ただ、それでもアイテムを回転させてカメラの方向に向ける動作は十分に実現出来る。

スクリプトの起動時にコルーチン処理を開始して、もしスリープ状態とスクリプトで判定すれば数秒おきに敵の頭上にプレハブをインスタンス化する。

コルーチンを使用するとき、ローカル変数にコンポーネントをキャッシュする方法があることに注目してほしい。

プレハブそのものは単にテキストメッシュとメッシュレンダラ、時間の経過とともに上昇するようにな」っているスクリプト、シーン1のBasicLookAtPlayer スクリプトから構成される。

テキストを上昇させるスクリプトもまた自明で簡単なものだ。
using UnityEngine;
using System.Collections;

public class GentleRiseAndDestroy : MonoBehaviour {

    public float timeToLive = 2f;
    Transform _transform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        Destroy(gameObject, timeToLive);
    }

    // Update is called once per frame
    void Update () {
        _transform.position += Vector3.up * Time.deltaTime;
    }
}
スクリプトは数秒後にオブジェクトを破棄して、毎フレーム毎にゆっくりとテキストを上昇させる。

期待している動作は Zzzzzz の記号が、カメラの位置に関わらず画面上に表示されることだ。プレイヤーが動くことでテキストの裏側に回り込めるのは論外だ。しかし、ここで解決しないといけない課題がいくつかある。

まず Zzzzzz は逆向きに表示されるが、その理由はUnityによりテキストメッシュの forward がカメラから離れる方向と判断してテキストを左から右へと読めるようにしようとするためだ。

Unity では、オブジェクトの向きに関して変な仕様がいくつかある。Unity で生成した Plane は forward ではなく up に向く。また、テキストメッシュは通常と逆に感じる - forward 方向に向く。

敵に近づいたとき、もう一つの問題に気付くことになるが、Zzzzzz は画面から外の方向に向かない。多くの人がこれで混乱する (私もその一人だった) 。LookAt 関数でカメラに向けたつもりが「何で上手く行かないの?」、と。

Transform ベクトルの修正


LookAt の問題は、実際にはカメラの位置をみていることだ。しかしカメラはワールド座標の固定位置にありながら、それと同時に Focal Area = 焦平面(シャッターの範囲)にある映像が投影される仕組みだ。オブジェクトは自らの位置にもとづいて固定の位置をみているので、カメラによりレンダリングされる位置とズレが生じることを学習する必要がある。

チュートリアル シーン6

このあたり巧いテクニックがある。但しいつでも使えるものではなく次のセクションでは別のアプローチも見ていきたい。transform.forward によりワールド座標でのVector3.forward と同様にオブジェクトの向きを表現できることはご存知のはずだ。意外にも知られていないのは transform.forward をセットしてオブジェクトを回転出来ることだ。

次にやるべきことはカメラの位置を見ることをやめて、テキストメッシュにカメラのFocal Area(焦平面)への向きを定義することだ。

テキストメッシュが反転されてることを覚えてほしい。通常のモデルであれば -transform.forward としないとカメラと逆方向を向いてしまう。
using UnityEngine;
using System.Collections;

public class ReversedCameraDirection : MonoBehaviour {

    Transform _transform;
    Transform _cameraTransform;

    // Use this for initialization
    void Start () {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }

    // Update is called once per frame
    void Update () {
        _transform.forward = _cameraTransform.forward;
    }
}
Zzzzz のスリープ状態テキストのプレハブに ReverseCameraDirection を BasicLookAtPlayer の代わりにアタッチすることでようやく準備は万端だ。

transform のベクトルを forward, up, right の方向をセットする機能は強力であり、回転でなくベクトルをベースに考えることが出来て便利だが、注意すべきことがある。これらの値の一つをセットした場合、他のベクトルは制御出来ておらず、ときどきおかしな向きに反転したり回転していることがある。
-------
次に続くぜ!


関連記事

AI開発&クォータニオン入門~3.敵キャラをプレイヤーに向かせる

関連記事

では例のごとく、Unity Gemsからの翻訳をどうぞ!

--------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012


敵キャラをプレイヤーに向かせる方法


最も良くあるケースは、何か別のものを見させるということだ。3D宇宙空間など全ての角度から見られるケースは特に問題がない。しかし、敵キャラを地面の上に立つ時は問題が生じるケースがある。


チュートリアルのシーン1


敵キャラに何かを見させる時、明らかにやらないといけないことは、Transform.LookAt(指定位置)  を使用することだ。最初のサンプルではプレイヤーのカプセルがあり、カメラをアタッチしてある。標準的なFPS入力で制御して移動し、MouseLook コントローラによりビューが回転できるようにしてある。

シーンには4人の敵がいるとする。最初のシーンでは敵の動作は単純で、プレイヤーの方を見るだけだ。 BasicLookAtPlayer.cs というスクリプトを使用することにする:
using UnityEngine;
using System.Collections;
 
public class BasicLookAtPlayer : MonoBehaviour {
    // Update is called once per frame
    void Update () {
        transform.LookAt(Camera.main.transform.position);
    }
}
見て分かるとおり、これは標準的なLookAt コマンドを使用して敵キャラはメインカメラの方向、すなわち今回の場合はプレイヤーを見る。残念ながらこの手法はプレイヤーと敵で高さが異なる位置にいる場合に欠点がある。結果的に敵キャラが不自然に仰向けとなる可能性がある。

Y軸周りのみを回転


やらないといけないことは、プレイヤーの方を向くように敵をY軸周りに回転させることであり、X軸やZ軸も回転できるようにすると不自然な方向に傾くことになる。

チュートリアルのシーン2

安心してほしいが、これを実現するのは難しくない。このシーンでは LookAtPlayerOnOneAxis スクリプトを使用する。
using UnityEngine;
using System.Collections;

public class LookAtPlayerOnOneAxis : MonoBehaviour {

    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
        newRotation.x = 0;
        newRotation.z = 0;
        transform.rotation = Quaternion.Euler(newRotation);
    }
}
はじめに、敵の現在位置からメインカメラ(プレイヤー) に向かう方向の新規のクォータニオンを作成する。Quaternion.LookRotation は引数として direction (向き) 、すなわちカメラの位置から敵の位置を引算して得られる向き、を使用する。クォータニオンを作成した後、Vector3 角度の表現に戻すためには eulerAngles を使用してnewRotation に格納する。 作成した回転は最初のシーンで LookAt メソッドを使用して得られたものと同一の結果となる。 これをy 軸だけの回転にするためには単にx およびz 軸を0 にセットする。次にQuaternion.Eulerを用いて更新された値をクォータニオンに戻す。

回転をスムーズにする


現時点では敵はプレイヤーの方向に一瞬でピタリと向くだけだ。これは現実的ではなく敵がシーンを動き回り始めると奇妙に映る。

チュートリアルのシーン3

Quaternion.Slerpを使用して敵現在のキャラクターの回転とプレイヤーの方向に向いたターゲットの回転の間の動きを補間できる。
using UnityEngine;
using System.Collections;

public class SmoothLookAtPlayerOnOneAxis : MonoBehaviour {

    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(Camera.main.transform.position - transform.position).eulerAngles;
        newRotation.x = 0;
        newRotation.z = 0;
        transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.Euler(newRotation), Time.deltaTime);
    }
}

ここでもまた、LookRotation を使用して回転を行い、x およびz の角度をゼロにする。次にSlerpを使用して現在の回転とターゲットの間を補間する。

Slerp は3 つのパラメータをとる。最初が開始時の回転、二つ目は終了時の回転、三つ目は 0 と 1 の間の値であり、開始あるいは終了にどれだけ近いかを示す。つまり関数の結果は、値が 0 のときは開始時点の回転と等しく、値が 1 のときは終了時点の回転と等しく、値が 0.5 のときは二つの回転の中間値と等しくなる、という要領だ。

もし開始と終了を結んだ直線上の線形補間でない位置をセットしたい場合、値を緩和 (ease)する関数を適用することが出来る。ease 関数は 0 と 1 の間の値をとる引数をとり、 戻す値も 0 と 1 の間だが、戻り値は曲線にもとづく。これを使用するメリットは、最初の動作は遅く、途中から速くして、最後にまた遅くする動きが実現できることだ。さらには関数を組んで好きなように調整が出来る。

今回のケースでいえば、毎フレームごとに潜在的な回転を更新したいため、開始と終了時点およびどれぐらいの速さで移動させるべきかが明確ではない。回転をスムーズにみせるために現在の回転を使って終了時の回転に毎フレーム更新する。これを行うために三つ目の引数にTime.deltaTime をセットする。

このメリットとして、回転の初期は素早く回転することだ(キャラクターと敵の角度の差が大きいため)。Time.deltaTime を使用する意味はターゲットの回転に向かって毎フレーム n% だけ近づくことである(Time.deltaTime が0.1秒なら、n% は10%といった具合)。次のフレームでターゲットの回転により近づき(キャラクターは動いてないと仮定)、差の n% だけ移動する。ターゲットの回転に近づくにつれて、回転速度は遅くなり、徐々に速度が緩やかになる見映えの良い動作が得られる。

回転がターゲットに近づくにつれて差は小さくなるが、それでも n% だけ近づくため、全く同じ値となるまではかなり長い時間がかかる。見た目は良くなるが、回転が全く同じとなるのをチェックするのでは、常に動き続けるため失敗することになる。

最大速度でのスムーズな回転

先ほどの例での問題は、回転が大きく異なる場合はキャラクターが急に速い回転をするため不自然な動作になることがある。そこで、最大の回転速度をある角度毎秒に限定する方法が必要となる。
チュートリアルのシーン4

四つ目のシーンでは RotateToPlayerWithAMaximumSpeed 関数を使用する。
using UnityEngine;
using System.Collections;
 
public class RotateToPlayerWithAMaximumSpeed : MonoBehaviour {
 
    public float maximumRotateSpeed = 40;
    public float minimumTimeToReachTarget = 0.5f;
    Transform _transform;
    Transform _cameraTransform;
    float _velocity;
 
    void Start()
    {
        _transform = transform;
        _cameraTransform = Camera.main.transform;
    }
 
    // Update is called once per frame
    void Update () {
        var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
        var angles = _transform.rotation.eulerAngles;
        _transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
            angles.z);
    }
} 
この先は、パフォーマンスの最適化についても検討する。カメラおよび敵のTransform を取得するところを最適化する。青色ハイライト表示された行はこれらのプライベート変数をキャッシュするところを表す。

Transform を使用することを変数のアクセスと同様に捉えている人がいるかもしれないが、それは誤りだ。transform.position と記述したときは GetComponent<Transform>().position を短く記述していることと同様だ。繰り返しこの処理を行う場合はパフォーマンス上明らかなオーバーヘッドとなり、Transformおよびその他の同様の変数をキャッシュするのは良い習慣だ。

このため最大の速度で回転を行うためにはMathf.SmoothDampAngle をコールする。今回は回転の eulerAngles を組み合わせてY軸回転には新しい変数を用いる。SmoothDampXXXX 関数(Vector3 および Mathfに存在する)の働きは、現在の速度、速度の最大値およびターゲット時間を取得し て動作を完成させる。現在の速度はルーチンとして使用され、float 値を与えて格納する必要がある。ただし、自前でセットするわけではない。
Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed) 
maximumRotateSpeedは解説が必要と思うが、minimumTimeToReachTarget により回転の開始および終了はスムーズとなる。回転の値に 0 をセットすると maximumSpeed を超えない範囲で出来るかぎり早く終了し、値が大きいほど回転がより遅くなり、理想的なタイミングで継続的な回転が終わることが期待どおりの動きである。こうすることの長所はターゲット値に近づくにつれて回転がスムーズに遅くなることだ。
void Update () {
    var newRotation = Quaternion.LookRotation(_cameraTransform.position - _transform.position).eulerAngles;
    var angles = _transform.rotation.eulerAngles;
    _transform.rotation = Quaternion.Euler(angles.x, Mathf.SmoothDampAngle(angles.y, newRotation.y, ref _velocity, minimumTimeToReachTarget, maximumRotateSpeed),
        angles.z);
}
update のルーチン全体でターゲットの回転の計算処理をを行い、敵の現在の回転を取得して Y 軸の回転を SmoothDampAngle で置き換えて敵キャラをプレイヤーの方向に向くように回転させる。非常に便利で使い勝手の良い動作が得られる。
-------
次に続くぜ!


関連記事


AI開発&クォータニオン入門~2.入門編

関連記事

例のごとく、Unity Gemsからの翻訳をどうぞ!

--------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012

クォータニオン入門

クォータニオンはUnityで回転を表現するために使用される内部形式だ。(つまり変換するための行列とは別次元のもの)

そしてクォータニオンは多くの開発者がいきなりつまづくところだ。分からなくなる大きな要因はインスペクタ上の表示でもあり、初めて見るとき x、y、z軸のVector3 で表示されていてこれがミスリードだ。この表示は本当はクォータニオンではない。むしろ回転のオイラー角に当たる。クォータニオン自身は確かに、x、y、zというプロパティを本当に持っているのだが、ひどいことにそれらの値はインスペクタで表示されているものと一切関係がないことだ。(反論もあるかもしれないが、あえてここでは言い切ってみよう)

クォータニオンを作成する方法はいくつかある。角度のVector3 から作成するには 三つの回転軸をパラメータとしてQuaternion.Euler を使用するか、回転を表すVector3を使用する。一定方向に向けた回転を作成するにはQuaternion.LookRotation(directionVector) を使用する。このチュートリアルでは、進めて行きながらクォータニオンを作成する様々な方法や組み合わせをみていきたい。

さて、オイラー角およびクォータニオンの違いについて整理すると:

オイラー角:


長所:
  • 3Dベクトルを使用するので、理解しやすく視覚化も容易
  • クォータニオンで扱うのは4変数、こちらは3変数なので演算が速い
短所:
  • 回転により得られる結果が必ずひととおりでない。エンジンによって  (x,y,z) または (y,x,z) またはそれ以外の順番を使用することがある。結果は毎回異なる
  • ジンバル ロックが発生する。一つの回転が他と並んだとき、互いにロックされ、一つの軸が失われる。

クォータニオン:


長所:
  • ジンバル ロックの発生を回避できる
  • 回転を、複数の軸回転の組み合わせでなく、ターゲット ベクトル にもとづいて処理できる
欠点:
  • オイラー角以外にもう一つ値があるため僅かに遅い
  • スティーブン・ホーキングでもないかぎり完全には理解出来ない(笑
複数の回転は * 演算子により組み合わせることが出来る(このチュートリアルの後の方でかなり、役に立つ)。さらに Vector3 を修正するには、クォータニオンを乗算するが、ベクトルを回転した戻り値が返される。このクォータニオン演算の順番には意味がある。
var newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector;
+ (プラス記号) ではクォータニオンを追加、組み合わせることは出来ない。 *  (アスタリスク記号) で複数の回転を組み合わせる。

回転を表すのにVector3 の代わりにクォータニオンを使用する理由の一つはオブジェクトを回転させて、あるターゲットの値をセットするのに、3つの軸回転の値を動かすだけでは実現出来ないケースがあるためだ。これはオイラー角に影響するジンバル ロックと呼ばれる現象に起因するものであり、興味あるならばウィキペディアで調べると良いが、原因は値をひとつづつ個別に動かす必要があることに起因していて、特定の組み合わせにおいては、一つの次元による変更のために別の次元がターゲットの値に到達出来ないケースがある。クォータニオンはこの問題を解決するために、回転全体を基本的に四次元にてワンステップで実現する。

オイラー角で物事を考えるのは簡単だが、クォータニオンほど強力ではない。Unityにより二つの表現方法を互いに変換させることが容易なので使用する都度に最適な方法を選択することが可能だ。例えば、ローテーションをClamp (ある値の範囲に限定 ) したい場合、クォータニオンの .eulerAngles を取得すれば良く、それからMathf.Clamp の演算で Clamp 処理を適用してオイラー角を、.eulerAngles の値を更新するか、または新しいクォータニオンをQuaternion.Euler(指定した角度)にて作成することで、クォータニオンに再び変換すれば良い。

回転を組み合わせたいとき、例えば頭の傾きと体の回転を組み合わせたいとき、どちらのアプローチも使うことが出来るが、これらの回転はワールド座標軸を中心とした回転ではない場合、例えばキャラクターがさらに前にかがんでいるようなケースはクォータニオンを使用すべきだ。クォータニオンを使用して各々の軸の周りの回転を作成してそれらを組み合わせるのは実に簡単だ。

Unityはさらに便利な軸の定義として、transform.forward, transform.right および transform.up を使用することが出来て Quaternion.AngleAxis とともに使用することで既存のオブジェクト回転に重ねて適用することご出来る。

//回転する体のうえで頭を傾けるような動作
transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation;

AI開発&クォータニオン入門~1.目次

3Dゲーム開発で良く必要になるのが、プレイヤーに向かってくる敵キャラの存在だ。

そしてプレイヤーが移動したとき、敵キャラはターゲットとなる角度の修正を行う必要がある。そこでプログラミングするうえで必要な知識はクォータニオンと呼ばれるものだ。

いわゆるオイラー角での計算は分かるけど、クォータニオンは苦手・・・。そんな場合にUnity Gemsのこのチュートリアルをみてみると便利だろう。

クォータニオンの勉強だけでなく、Unityでダウンロードできるサンプルつきでコード解説が丁寧につている。

まずはどのようなものが出来るか見てもらいたい:
http://youtu.be/wU5RT2sXghQ

まあ、外国の方が作成している敵キャラや、上に表示されるテキストが日本人のテイストからするとやや雑だったりするとおもうが、そういった演出レベルは自ら改善することは簡単。ここで覚えてほしいのはキャラの動作を制御するプログラムの中身だ。

このUnity Gemsというウェブサイトは、初心者のためにより有用な情報を集積しようという目的で運営されている。Unity Answersの常連が集まって、過去の投稿の中から選別されているだけあって、本当に優良なサイトだと感じていて、今回の投稿も評判は悪くない。

それではAIのスキルアップも兼ねて、Unity Gemsからの翻訳をどうぞ!
--------

http://unitygems.com/quaternions-rotations-part-1-c/
October 14, 2012

このチュートリアルで扱う内容


  • クォータニオンが便利な理由
  • クォータニオンを扱う上で良くある間違い
  • 敵キャラがプレイヤーの方を向く動作にリアリティを持たせる方法
  • テキストをカメラに向ける方法
  • 複数のクォータニオンを組み合わせる方法
  • プレーンで作成したビルボードをカメラに向ける方法
  • クォータニオンを使って敵AIのポジションをセットする方法
  • コルーチンを使用してアニメーションの完了を待つ方法
  • コルーチンで動かす基本的なステートマシンを作成する方法

チュートリアル プロジェクト


このチュートリアルで扱うプロジェクトファイルはダウンロードが出来る:

リンクからダウンロード


ダウンロードしたUnityのプロジェクトは、各々のシーンがこのチュートリアルの内容に対応するように構成されている。

各々のシーン、同梱スクリプト、オブジェクトはTutorial/Part1以下に配置されているのでそこからアクセスすると良い。

そして、このチュートリアルの中で、次の記号で表示するときは、別のシーンに移動して説明が始まるということだ:
チュートリアルのシーン1

チュートリアルはアセットストアにある、いくつかの無料アセットを使用しているので、その素晴らしい機能を提供してくださった方にも感謝したい。
--------

ブックマークに追加

このエントリーをはてなブックマークに追加

自己紹介

自分の写真
Unity3D公式マニュアル翻訳やってる人がスマホ(iPhone, Android)のゲーム開発しています。気軽に面白く初心者が遊べる内容がモットー。Blogでは開発情報をひたすら、Twitterではゲーム作成の過程で参考にしている情報を中心につぶやきます

ページビューの合計

過去7日間の人気投稿