Arbor: State Diagram Editorを試す - 縦STGの敵機を作ってみる

ケットシーウェアさんのArbor: State Diagram Editorというアセットを試食してみたのでその記録。

前置き

Unityで2.5D縦STGを作ろうという実験を半年以上超スローペースで続けているのですが、以前からずーっと困っているのが敵キャラの挙動です。単純な地上敵ならパス+スクリプトで弾を適当に撃つ、ぐらいで済むわけですが、中型機やボスキャラのように手の込んだものが必要となるとスクリプト直書きはちょっときついし増産にも向かない、ということで、ビジュアルスクリプティングでどうにかならんものか、とアレコレしていました。

Arborの前にはド定番のPlayMakerでいくつか作ってみたのですが、

  • ステートにぶら下げるアクションがデフォルトで凄く揃ってる
  • アクションのパラメータを変数で与えられるので外部パラメタの導入もしやすい

その一方で、

  • 全部アクションで書いちゃうと冗長になって後から何やってたのか読めなく
  • ありもののアクションの組み合わせばかりだとステート遷移が増えがちで見通しがドンドン悪くなる
  • かといって新規にアクションを作るのも面倒(独自のお作法が多い)

ということでちょっと使いにくいと感じていたところでした。

ちょうどそこにキャンペーンでArborが試せるということで、試食してみた次第です。

……本当は8月中に上げるはずだったんですが……

導入

アセットストアで購入してインポート。難しい事はありませんが、更新時は必ずArborのフォルダを削除してからインポートするようにと指示があります。
また、1.7.4での話ですがPro Draw call Optimizer Lightと一部クラスが名前衝突していてコンパイルが通らなくなっていました。
Pro Draw call Optimizer Lightを削除すれば問題なし。

試食

下ごしらえ

まず敵機を用意します。

f:id:dnasoftwares:20150905112240p:plain

どこにでもある中型機です。DoGA-L3で組み立ててPLAY Animation Importer for Unityでインポートしました。doga.jp

Unity5にまだ対応してないので、Unity4.6でインポート→スケール調整→まとめてunitypackageに→Unity5に持ち込み と作業します。

で、コンポーネントをペタペタと。オレオレコンポーネントがありますが本筋ではないので下記説明程度で。

f:id:dnasoftwares:20150905112544j:plain

敵機の基本的な動作は専用コンポーネントに分離。Arborで動かすのはあくまで移動と攻撃だけに絞ります。

ステートマシンをつくる

よくある中型機として、出現→攻撃→逃走 の流れを作ります。今回は攻撃1種だけでは単純すぎるのでライフの残量をみて発狂フェイズに移行するようにしました。
先に完成図を挙げます。

f:id:dnasoftwares:20150905125129j:plain

「開始ステート」になっている左上の状態からスタートして、コード内で状態遷移が発生したら矢印の先に進みます。
ステートごとに1つ以上の「挙動」を追加できます。挙動1つ=MonoBehavior1つ みたいなノリです。実際はStateBehaviorというクラスの派生クラスになります。
スクリプトの作成は普通のコード作成同様にプロジェクトビューで。プロジェクトビューの右クリックメニュー、「Create」の内側に「Arbor」が増えており、そこからStateBehaviorの雛形を新規作成できます。
開始ステートはAppearとあるように出現処理です。EnemyMid2Appearというコードをひっつけています。
独自のコントローラを使ってるのでアレですが説明を入れてるのでそういう処理をしていると適当に理解いただければと思います。

EnemyMid2Appear.cs
using UnityEngine;
using System.Collections;
using Arbor;
using DG.Tweening;

public class EnemyMid2Appear : StateBehaviour
{
    //次のステート(エディタで設定する)
    public StateLink NextState;

    //コントローラへの参照
    private EnemyController _ctrl;
    // Use this for initialization
    void Start()
    {
    }

    // Use this for enter state
    public override void OnStateBegin()
    {
        transform.eulerAngles=new Vector3(0,0,0);
        _ctrl = GetComponent<EnemyController>();
        _ctrl.LocalVelocity = new Vector3(0, 0, -58.0f); //この機体に画面下に向けて降りてくるベクトルを設定
        StartCoroutine(StateThread()); //以下コルーチン任せ
    }

    IEnumerator StateThread()
    {
        while (transform.localPosition.z>=80.0f) //この機体が画面上1/4ぐらいのラインに下ってくるまで
            yield return new WaitForFixedUpdate(); //待機
        Transition(NextState); // 次ステートへ遷移
    }

    // Use this for exit state
    public override void OnStateEnd()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

ジャンプ先のステートを格納するpublic StateLink NextState;と実際に遷移を実行するTransition(NextState);がキモかと。
StateLinkの変数を定義するとArbor Editorにリンクとして表示され、次のステートへD&Dの操作で矢印を引けるようになります。

普通にStartCoroutineが呼べるのでかなり書きやすいです。

図で飛び先となっているLoopステートの挙動はこちら。

EnemyMid2Loop.cs
using UnityEngine;
using System.Collections;
using Arbor;
using DG.Tweening;

public class EnemyMid2Loop : StateBehaviour
{
    public StateLink GoMadState;    //発狂モード突入時の飛び先(エディタで設定)
    private EnemyController _ctrl;  //コントローラへの参照
    public GameObject[] Firepoint;  //敵弾の発射地点を示すGameObject(エディタで設定)
    public GameObject BulletObject; //敵弾として吐き出すGameObject(エディタで設定)
    [Range(0,100)]
    public float GoMadLifePercent=30.0f; //発狂モードに突入するライフ%値(0~100)

    private Coroutine _cFireController;

    // Use this for initialization
    void Start () {

    }

    // Use this for enter state
    public override void OnStateBegin() {
        _ctrl = GetComponent<EnemyController>();

        DOTween.To(() => _ctrl.LocalVelocity.z, x => _ctrl.LocalVelocity.z = x,
            0, 1.0f); //前ステートでかかっていたzベクトル(縦方向速度)を1秒かけて0にする
        _cFireController = StartCoroutine(FireControllerThread()); //攻撃制御開始
    }

    private IEnumerator FireControllerThread()
    {
        yield return new WaitForSeconds(0.4f); //最初の1セットの前に0.4秒待つ
        while(true)
        {
            Transform player = GameEnvironment.GetNearestPlayer(transform.position); //自分から近い方のプレイヤーの場所を得る
            for (int i=0;i<3;i++)
            {
                //FirePointそれぞれから……
                foreach(GameObject fp in Firepoint)
                {
                    //プレイヤーに向けて3Way弾を撃つ
                    float angle = DNASoftwares.Unity.MathEx.AimProjected
                        (fp.transform.position, player.position); //近い方のプレイヤーの角度を得る
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle, 110); 
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle-22.5f, 110);
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle+22.5f, 110);
                }
                for (int j = 0; j < 8; j++) yield return new WaitForFixedUpdate(); //8フレームおき
            }
            yield return new WaitForSeconds(1.0f); //1セット撃ったら1秒待つ
        }
    }
    
    // Use this for exit state
    public override void OnStateEnd() {
        //遷移時には今の攻撃スレッドを停止させる
        StopAllCoroutines();
    }
	
    // Update is called once per frame
    void Update () {
        //ライフがしきい値を超えたら(_ctrl,LifePercentは自分の残ライフを百分率で返す)
        if (_ctrl.LifePercent <= GoMadLifePercent)
        {
            //発狂ステートへ
            Transition(GoMadState);
        }
    }

    // このステートの間動き続ける
    void FixedUpdate()
    {
        //横方向にゆらゆら動く動作
        _ctrl.LocalVelocity.x = Mathf.Sin(_ctrl.TimeElapsed/2.0f*Mathf.PI)*10.0f;
    }
}

FixedUpdateも書けば普通に動いてくれます。MonoBehaviorとまったく同じ挙動です。
また、StateLinkに限らずpublic変数はインスペクタ同様に表示・編集できますしアトリビュートもバッチリです。新しい事を覚える必要がほとんどないので取り組みやすいと思います。

発狂後の方はやってること自体は弾撃ってるだけなので本筋と外れてしまうので省略。

常駐ステートで割り込みをかける

一定時間で逃げる、という処理は色々書き方もあるかと思いますが、今回は「常駐ステート」を使ってみることにしました。

図の右下の緑色のステートが「常駐ステート」で、このステートの挙動は文字通り他のステートと別に常駐して動作します。
ここにタイマーを仕込んでおけば出現から一定時間(ここではSeconnds=12なので12秒)経過すると指定ステートに割り込みで遷移してくれるという手はずです。

あくまで今のステートに割り込む形ですので、常駐ステートの先も並列実行になるわけではありません。

逃げる挙動はAppearの逆でしかないので省略。

動かしてみる

実際に上図ステートマシンを動かした様子がこちらのツイートにあります。
オレンジ色のステートがそのとき動いているステートです。


所感

ということで非常に取り組みやすいなと思いました。デフォルトで付属してくる挙動スクリプトの量はPlayMakerには遠く及びませんが、自力でコードが書けるなら大した問題ではありませんし、
MonoBehaviorと同じノリでコードが書けますのでむしろ取り組みやすいと思います。
ステートをまたいだ変数の扱いなどが悩むところですが、専用にコンポーネントを作ればなんとかなるかと思います。RequireComponent属性で紐付けるとかすれば確実でしょう。

今回は手抜きして状態遷移とアクションを全部1スクリプトに押し込んでしまいましたが、状態遷移に関わるスクリプトと、実際になにか行動を起こすためのスクリプトは分割すると使い回しが効いていいと思います。

あと申し上げたいこととしては、チュートリアルが全編動画になっているのが個人的にはツラいです。動画だと自分のペースで進められないのと、Arborの場合はエディタの説明やら常駐ステートの説明などが動画の中に埋まっているため引用とかしづらい……(この記事書くのに困った)
ただリファレンスマニュアルは別途しっかりまとまっているので1回通してしまえば後の苦労は無いかと思います。

定価$95のPlayMakerと比べてArborなら定価$19.99とほぼ1/5のお値段で必要十分な機能が揃えられるので、自分でコードが書ける上で状態遷移の部分をスッキリさせたいという向きには十分にオススメできると思います。

Unity5のStandardシェーダのパラメタをスクリプトからいじろうとして丸一日潰れた話

Unity遊びの記録を書かずにまごまごしていたらUnity5になってしまって大変申し訳ない気分でいっぱいです。

お詫びの印じゃないんですが今やっている別ネタで発生した小ネタについて書き残しておきます。誰か既にやってると思ったら誰も書いてない、という……

Unity5を触った方ならご存じかと思いますが、Unity5からはだいたいの材質は物理ベースの統合シェーダ一つで表現されるようになりました。

docs.unity3d.com

このパラメータの中には自己発光を表す「Emission」が含まれています。
Emissionの値をいじることでモデルが自ら光るような表現ができるわけですが、これを応用してSTGでの与ダメージ表現をやってみようと思ったわけです。

ざっくり実装手順を。

スクリプトからマテリアルのパラメータをGet/Setするために、まずパラメータの内部名称を調べます。調べたいシェーダーを指定しているマテリアルをインスペクタで開き、インスペクタ右上隅の歯車マークをクリック。「Select Shader」を選びます。

f:id:dnasoftwares:20150319032129p:plain

そうするとシェーダーの情報が出ます。

f:id:dnasoftwares:20150319032134p:plain

左の赤枠、「_」から始まっている文字列がパラメータの名前です。
同じ行に型と、インスペクタでの表示名が出ています。表示名を参考に、いじりたいパラメタはどの名称かを調べます。

ここではエミッションをいじりたいので_Emissionなんたらというのを探しますが、
どうやら_EmissionColorが探していたもののようです。

じゃあこの_EmissionColorなるパラメータをいじればよさそう、ということで下記のようなコードを試したのですが……

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

ところがこれだとうまくいきません。最初からEmissionが指定されているマテリアル以外では色がつかないのです。

これの理由がわからなくてオロオロしてたのですが、マニュアルやらシェーダーのソースやらを掘り返してようやく理由がわかりました。

docs.unity3d.com

キモの部分だけ一部引用

Often it is convenient to keep most of a piece of shader code fixed but also allow slightly different shader “variants” to be produced. This is commonly called “mega shaders” or “uber shaders”, and is achieved by compiling the shader code multiple times with different preprocessor directives for each case.

In Unity this can be achieved by adding a #pragma multi_compile or #pragma shader_feature directive to a shader snippet. At runtime, the appropriate shader variant is picked up from the Material keywords (Material.EnableKeyword and DisableKeyword) or global shader keywords (Shader.EnableKeyword and DisableKeyword).

全訳はしませんが、"#pragma multi_compile"あるいは"#pragma shader_feature"がC#でいういわゆる#defineみたいなもので、キーワードの有効・無効を切り替えることでシェーダーのコンパイルを分岐できるということのようです。んで、スクリプトからキーワードを有効、無効にする手段があってそれがMaterial.EnableKeywordMaterial.DisableKeywordだと。

シェーダーのソースはUnityのダウンロードページで「ADDITIONAL DOWNLOADS」から「Built in shaders」を選べば入手できます。

Standard.shaderの一部引用。

#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP 
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _PARALLAXMAP

「#pragma shader_feature _EMISSION」どうやらこのあたりらしい、ということで下記のように書き直してみたところ、期待通りに赤色に光ってくれました。

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.EnableKeyword("_EMISSION");
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

例として1秒おきにEmissionを切り替える→元にもどす、を繰り返すコードを用意しました。
コルーチン使ってますけど別にいいよね?(1秒おきに云々とかUpdateで書くのめんどくさいし)

動く方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTester : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは動く例
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.EnableKeyword("_EMISSION"); //キーワードの有効化を忘れずに
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせる
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}
動かない方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTesterBadExample : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは悪い例(変化しない)
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせたい
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}

それぞれSphereに割り付けて、適当なStandardシェーダーをマテリアルにして実行したら下のような感じになります。
f:id:dnasoftwares:20150319095404g:plain
応用すればこんな表現に。(というかこれがやりたかった)

というわけで参考になれば幸いです。

Unityで2ヶ月あそんだ記録 (3)自機のコンポーネントとかGameObject構造とか

とりあえず最低限のネタを揃えたらモデルにコンポーネントをひっつけてコードを書く。まずは自機から。

自機の挙動は下記のような感じにするつもりでいた。ここは最初から変更なし。

  • 前進、後退、左右の各方向について移動性能に差がある(前進は早いが後退は遅いとか、前後より左右の方が素早く動けるとか、そういうアレ)
  • 武器はガン(弾数無限でオーバーヒートがある)とミサイル(弾数有限で発射間隔がある。ロックオンする。複数発撃てる機体もあるかも)
  • 出現・死亡処理などで独自の挙動が書けるようにデリゲートを用意する(※結局使わず)

UnityにおけるC#の扱いがよくわかってなかったりとかあったので、派生クラスは余り使わずコンポーネント複数くっつける設計にしていた。
プレイヤー機の場合はこんな感じ。
f:id:dnasoftwares:20141127225108j:plain
コンポーネント複数に分かれているのは、自機を複数の機種から選べるようにしようか、とか、タイマンゲーにするつもりだったころにCPU操作に入れ替えができるように、とか考えていたころの名残。

移動はCharacterControllerコンポーネントで。壁ズリとか何も考えずとも(いやそれなりに考える事はあるけど)そこそこちゃんと動いてくれるので助かる。ただ、Step Offsetをゼロにすると複数ポリゴンからなる床を滑らせたときに、平坦なはずの床でもひっかかって思い通りに動かなかったりするので0.05ぐらいにしておくのがよさそう。

自機弾の発射についてはUnity公式の2Dシューティングチュートリアル敵を作成しようの部分を参考にした実装にしている。

各種座標をキャラのGameObjectの子として保持させ、実行時には子をtransform.GetChild(n)で調べ上げ、特定の名前を持つtransformを基準座標にして弾を生成する。弾の発射方向もこのGameObjectの向きで表せばいいのだから便利である。
f:id:dnasoftwares:20141127234303j:plain
インポート時のデータ構造の関係でモデルデータが空のGameObjectの下にぶら下がる形になってしまっていた(上図でいえば"fourfoot"がモデル実体であり、親の"player"自身は何のレンダラも持っていない)が、回転等の中心、足の位置の微調整などはこっちの方がやりやすかったし結果オーライということで。

BasePlayerBehavior.cs(抜粋)
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System.Collections;

public delegate IEnumerator PlayerStartDelegate();
public delegate IEnumerator PlayerDeadDelegate();

[RequireComponent(typeof(LifeProperty))]
public class BasePlayerBehavior : MonoBehaviour
{

    //イベントを別スクリプトから登録できるようにして登場時・死亡時処理を書けるように
    //したかった(結局使ってない)
    public event PlayerStartDelegate OnPlayerStart; 
    public event PlayerDeadDelegate OnPlayerDead;

    //視点切り替え用の視点(カメラ)リスト
    public List<string> ViewsList;
    public int CurrentViewIndex = 0;

    //武器に関わるパラメタ 武器制御用のスクリプトに初期設定させる
    [System.NonSerialized]
    public float PrimaryWeaponHeat = 0; //ガンの現在のヒート値
    [System.NonSerialized]
    public float PrimaryWeaponHeatMax = 40; // ヒートの最大値(この値を超えるとオーバーヒート)
    [System.NonSerialized]
    public float PrimaryWeaponCoolDownSpeed = 10; // クールダウン速度(1秒間あたりに減少するヒート値)
    [System.NonSerialized]
    public int SecondaryWeaponCapacity = 1; // ミサイルの最大数
    [System.NonSerialized]
    public int SecondaryWeaponAmmo = 1; // ミサイルの現在の残弾数
    [System.NonSerialized] public float SecondaryWeaponReloadTime = 0; // ミサイルのリロード残り時間(0=発射可能)

    protected bool PrimaryOverHeat = false;

    //ミサイルのロックオンに関わる処理・関数類は省略

    // Use this for initialization
    IEnumerator Start()
    {
        if (OnPlayerStart != null)
        {
            yield return StartCoroutine(OnPlayerStart());
        }
    }
	
    // Update is called once per frame
    void Update () {
        //時間で変化するパラメータ(といってもガンのヒート値とミサイルのリロード時間だけだが)を処理
        if (PrimaryWeaponHeat > 0)
        {
            PrimaryWeaponHeat -= PrimaryWeaponCoolDownSpeed*Time.deltaTime;
            if (PrimaryWeaponHeat <= 0) //ヒートが0になったらオーバーヒートフラグも下げるよ
            {
                PrimaryWeaponHeat = 0;
                PrimaryOverHeat = false;
            }
        }
        if (SecondaryWeaponReloadTime > 0)
        {
            SecondaryWeaponReloadTime -= Time.deltaTime;
            if (SecondaryWeaponReloadTime < 0)
            {
                SecondaryWeaponReloadTime = 0;
            }
        }
    }

    //武器パラメータの初期設定
    public void SetWeaponParams(float heatmax, float cooldown, int maxsecondary)
    {
        PrimaryWeaponCoolDownSpeed = cooldown;
        PrimaryWeaponHeatMax = heatmax;
        SecondaryWeaponCapacity = SecondaryWeaponAmmo = maxsecondary;
    }

    //ガンはオーバーヒートしてるかを返す(外部からパラメタを直で触る事故がないように隠蔽)
    public bool IsPrimaryOverHeated
    {
        get {
            return PrimaryOverHeat;
        }
    }

    //ヒートを増加させる(オーバーヒートフラグの管理含む)
    public bool AddHeat(float f)
    {
        PrimaryWeaponHeat += f;
        if (PrimaryWeaponHeat >= PrimaryWeaponHeatMax)
        {
            PrimaryWeaponHeat = PrimaryWeaponHeatMax;
            PrimaryOverHeat = true;
        }
        return PrimaryOverHeat;
    }

    // 自機パラメータをどこからでも取得できるようにする関数
    static public BasePlayerBehavior FindPlayer()
    {
        return GameObject.FindGameObjectWithTag("Player").GetComponent<BasePlayerBehavior>();
    }
}
PlayerMovementController.cs (抜粋)
using System;
using UnityEngine;

[RequireComponent(typeof(CharacterController)),
RequireComponent(typeof(BasePlayerBehavior))]
public class PlayerMovementController : MonoBehaviour {

    //前進・後退・左右・旋回それぞれについて、最高速・加速度・減速度を定める
    public float ForwardMaxSpeed = 24.0f;
    public float ForwardAccel = 12.0f;
    public float ForwardBrake = 40.0f;
    public float BackwardMaxSpeed = 18.0f;
    public float BackwardAccel = 10.0f;
    public float BackwardBrake = 40.0f;
    public float SlideMaxSpeed = 24.0f;
    public float SlideAccel = 12.0f;
    public float SlideBrake= 40.0f;
    public float TurnMaxSpeed = 3.3f;
    public float TurnAccel = 30.0f;
    public float TurnBrake = 9.5f;

    //前後、左右、回転それぞれの現在速度。
    protected float ZCurrSpeed = 0.0f;
    protected float XCurrSpeed = 0.0f;
    protected float RCurrSpeed = 0.0f;

    // Update is called once per frame
    void Update ()
    {
        CharacterController controller = GetComponent<CharacterController>();
        float jX = Input.GetAxis("Horizontal");
        float jY = Input.GetAxis("Vertical");
        float jR = Input.GetAxis("Rotation X");
        //ジョイスティック入力→目標速度
        float vZGoal = jY*(jY>0?ForwardMaxSpeed:BackwardMaxSpeed); //前入力か後ろ入力かで最高速を切り替え
        float vXGoal = jX*SlideMaxSpeed;
        float vRGoal = jR*TurnMaxSpeed;
        //目標速度と現在速度をマッチング
        RCurrSpeed = Mathf.MoveTowards(RCurrSpeed, vRGoal,
	        (is_braking(RCurrSpeed, vRGoal) ? TurnBrake : TurnAccel)*Time.deltaTime);
        XCurrSpeed = Mathf.MoveTowards(XCurrSpeed, vXGoal,
	        (is_braking(XCurrSpeed, vXGoal) ? SlideBrake : SlideAccel)*Time.deltaTime);
        ZCurrSpeed = Mathf.MoveTowards(ZCurrSpeed, vZGoal, (jY > 0 ? 
            (is_braking(ZCurrSpeed,vZGoal)?ForwardBrake:ForwardAccel) :
            (is_braking(ZCurrSpeed,vZGoal)?BackwardBrake:BackwardAccel) )
            *Time.deltaTime);
        //マッチング後の速度を各要素に加算
        //回転
        float ry = transform.eulerAngles.y;
        ry += RCurrSpeed*Time.deltaTime;
        transform.eulerAngles=new Vector3(0,ry,0);
        //移動 移動量をVector3にセットしてCharactorController.Moveで移動実行
        Vector3 vmove = new Vector3(XCurrSpeed,0, ZCurrSpeed);
        vmove = transform.rotation*vmove;
        vmove += Physics.gravity;
        CollisionFlags f = controller.Move(vmove*Time.deltaTime);
    }

    //現在速度と目標速度を比較して「ブレーキをかけるのか」を判定する関数
    //(加速度と減速度で違う値を当てるために使う)
    private static bool is_braking(float current, float goal)
    {
        if (current > 0)
        {
            if (current > goal) return true;
        }
        else
        {
            if (current < goal) return true;
        }
        return false;
    }
}
WeaponControllerFourFoot.cs(抜粋)
using UnityEngine;
using System.Collections;

public class WeaponControllerFourFoot : MonoBehaviour
{
    public GameObject BulletObject; // 飛ばす弾のオブジェクト
    public GameObject MissileObject; // ミサイルのオブジェクト
    public float FireInterval = 0.1f; // ガンの発射間隔
    public float MissileInterval = 2.0f; // ミサイルの発射間隔
    protected bool isPrimaryFiring = false; //発射ルーチンの多重起動防止フラグ
    protected bool isSecondaryFiring = false; // 同上
    private BasePlayerBehavior _player = null; // 各種パラメタを突っ込んでいるBasePlayerBehaviorへの参照
    public int MissileCapacity = 5; //ミサイル最大保持数
    public float MaxHeat = 50; // ヒート限界値(ガンがオーバーヒートするまでの長さ 大きいほどオーバーヒートが遅い)
    public float CooldownTime = 4; // ヒートのクールダウン時間(ヒート最大から0に戻るまでの秒数で表現)

    // Use this for initialization
    void Start()
    {
        _player = GetComponent<BasePlayerBehavior>(); // BasePlayerBehaviorを取得
        _player.SetWeaponParams(MaxHeat, MaxHeat/CooldownTime, MissileCapacity); //BasePlayerBehaviorに各種パラメタを送信
    }

    // Update is called once per frame
    void Update ()
    {
        if (!_player.IsPrimaryOverHeated && !isPrimaryFiring && Input.GetButton("Fire1"))
        {
            StartCoroutine("FirePrimary");
        }
        if (Input.GetButton("Fire2"))
        {
           if(_player.SecondaryWeaponAmmo > 0 && !isSecondaryFiring && _player.SecondaryWeaponReloadTime==0)
               StartCoroutine("FireSecondary");
        }
    }

    //ガンの発射処理
    //発射処理はコルーチンでやるほうが間隔をおく処理を簡単に書ける
    IEnumerator FirePrimary()
    {
        isPrimaryFiring = true; //ガン発射中フラグを立てる
        while (Input.GetButton("Fire1")) //Fire1が押されてる間はひたすら連射
        {
            for (int i = 0; i < transform.childCount; i++)
            {
                Transform shotpos = transform.GetChild(i); // 自分の子GameObjectを順番に調べる
                if (shotpos.name == "Cannon_spawner") //そのオブジェクトの名前がCannon_spawnerだったら発射処理
                {
                    GameObject o = Instantiate(BulletObject, shotpos.position, shotpos.rotation) as GameObject; 
                    _player.AddHeat(1.0f); //ヒート値を足す
                    if (_player.IsPrimaryOverHeated) //オーバーヒートした?
                    {
                        isPrimaryFiring = false;
                        yield break; // 打ち方やめ(コルーチン脱出)
                    }
                    //1発出したら発射間隔の時間だけ待つ、2箇所の発射点があれば交互に弾が出る仕掛け
                    yield return new WaitForSeconds(FireInterval); 
                }
            }
        }
        isPrimaryFiring = false;
    }

    //ミサイルの発射処理は良く考えたらコルーチンでやる理由がなかった……
    IEnumerator FireSecondary()
    {
        isSecondaryFiring = true;
        for (int i = 0; i < transform.childCount; i++)
        {
            Transform shotpos = transform.GetChild(i);
            if (shotpos.name == "Missile_spawner")
            {
                GameObject o = Instantiate(MissileObject, shotpos.position, shotpos.rotation) as GameObject;
                //本当はロックオンしている敵をミサイルに教える処理があるが省略
            }
        }
        _player.SecondaryWeaponAmmo--; //残弾数減らす
        _player.SecondaryWeaponReloadTime=(MissileInterval); ミサイルのリロード時間を設定
        isSecondaryFiring = false;
        yield break;
    }
}
LifeProperty.cs
using UnityEngine;

public class LifeProperty : MonoBehaviour
{

    public float Life = 1.0f;
    private float _maxLife = 1.0f;
    public float MaxLife
    {
        get { return _maxLife; }
    }

    // Use this for initialization
    void Start ()
    {
        //ライフの初期値を最大値ということにして記憶。
        _maxLife = Life;
    }

    // Update is called once per frame
    void Update () {
    
    }

    //回復処理はこの関数を通す。ライフ値が初期のライフ値以上にならないようにしている
    public void Recover(float inc)
    {
        Life += inc;
        Life = (Life > MaxLife) ? MaxLife : Life;
    }
}

Unityで2ヶ月あそんだ記録 (2)ネタ決め、素材集め(最初期)

Unityで遊ぶといっても目的もなしに遊べるわけがないわけでして。とりあえずなんでこのネタになったのか、から。

ネタ決め

最初から色々やっても上手くいくわけがないので、とにかく手のかからないモノで一度やってみようということに。

  • 初っ端から立体機動バリバリは死亡フラグなので平面上で完結するもの
  • モーション云々が絡む人物・ロボット等は出さない(モデルもってないし)

ということでとりあえず平面で撃ち合うシューティングという方向性だけ決めて、まず組めるところまで組んでしまおうということにした。当初は某アーケード戦車ゲーよろしくタイマンにするか、と思っていた。

素材集め

モデル類

ウチはプリレンダのころからちょくちょくDoGA-L3でキャラ作ってた(というかLシリーズ以外で作れる気がしない)ので、今回もなんとか使えないものかと試行錯誤。とてかんCGコンバータが出たので今後はUnityで完結するのだが、参考までに当時の変換手順を。

DoGA-L3には組み立て後のモデルデータを.SUF(DoGA CGA Systemの標準モデル形式)で出力する機能がある。また、Metasequoia(Ver3系)はSUF形式を読める貴重なモデラーで、出力形式も豊富。

ということで、

  1. L3でモデルを組み立ててSUF出力
  2. Metaseqで読み込んで適当な形式で再出力(ウチはDAEを使用)
  3. それをUnityにインポートして
  4. マテリアル微調整で完了

というのが基本の手順だった。
インポート直後はモデルの各面がスムージングされててあまり見た目よくないのでスムージングを切る。
f:id:dnasoftwares:20141121111341j:plain
Import SettingsでNormals、Tangentsをそれぞれ【Calculate】にし、Smoothing Angleを0にすればいい。

超単純な機銃の弾のモデル(正四面体をZ方向だけ超引き延ばして終了)などはMetasequoiaで直接モデリングして出力。

エフェクト・スプライト

最初はとにかく「すぐ表示できること」を優先で、セールだかなんだかで買っていたWar FXを入れてた。

War FX by Jean Moreno (JMO) -- Unity Asset Store
が、全体的にフラットシェーディングの絵作りなのにそこだけ今風というのもおかしいので、スプライトっぽさを出すべく2Dの爆発アニメを入れることに。

元絵はフリーやCC-BYとかの素材から。

効果音・BGM類はリリース決めた後の最終盤まで入れてなかった。

Unityで2ヶ月あそんだ記録 (1)とりあえず淡々とタイムラインを並べてみる

デジゲー博2014で「METROWARS」っていうゲームを(体験版で)出したんですがここ2ヶ月に何やってたのかをまとめて振り返ってみたいなと。数日間ダラダラと所感を書いてみる予定。

続きを読む

夏休みの自由研究:「右スティック」の軸について調べてみる(おまけでボタン番号割り当ても)

※間が開いてしまいましたが感想というかまとめを最後に書きました。

前置き

AIMSの新バージョンで「アナログ入力」にも真面目に対応しよう、ということで折角なんでアナログ2軸×左右スティック分くらいはとれるようにしたいなと思ったわけです。
しかし左スティックはXY軸で決め打ちでいいとしても右スティックがわからない。なんか昔は軸定義がバラバラでなかなか悲惨だったと聞くので、ここらで真面目に調べてみるかと思いました。ちょうどこんなゲームをつくることになった流れでゲームパッドをいくつか買い込んだので、こいつらの状況を調べてみることにします。
軸だけじゃなんなので物理的なボタンの位置と番号割り当ての違いも見てみようかなとか。

方法

各パッドをUSBポートに繋ぎ、コントロールパネルの「デバイスとプリンター」から「ゲーム コントローラーの設定」を呼び出し、対象パッドのプロパティで軸を調べます。とっても簡単。

実験環境

OS
Windows 7(x86)
メモリ
12GB*1
HDD
たくさん
VGA
それなりのGeForce
USB
マザーから延びてケースに出ているUSB2.0ポート
ドライバー類
記載ない限りOSが自動的にインストールする標準ドライバーを使う

被検体

実験に使ったのは以下のゲームパッド。

Logicool Rumble Gamepad F510

有線、振動付き、スイッチ切り替えでDInput、XInputどちらにも認識させられる。最近増えてきてますよね両対応品。
十字キーの扱いはMODEボタンでPOV、XY軸と切り替えることができる。

Logicool Wireless Gamepad F710

F510の無線版といってよさそう。無線で電池駆動な以外の諸元はほぼ同じ。DInput/XInput切り替えも可能。

ELECOM JC-U3613M

エレコムのDInput/XInput両対応パッド。連射設定ができる。
十字キーがPOVに固定。一頃昔の2Dゲームだとアナログスティックでデジタル操作をやるハメに……

iBUFFALO BSGP1204P

SAVIORというブランドのゲームパッド。PS3とPC両対応。PC接続時のみ、十字キーの扱いをPOVとXY軸で切り替えられる。

HORI HORIPAD3 PRO

ご存じホリのPS3用パッド。PS3用ではあるが、PCに挿しても動く。SIXAXISやDUALSHOCK3のように特殊なソフトで「起こす」必要もない。
斜め入力の感度設定や十字キーの傾き変更などユニークな機能満載。
十字キーがPOVに固定。もともとPC用途は想定されてないからしょうがないね。

結果

詰め込み詰め込みでちょっと見づらいですがご了承ください。
今回実験したパッドはすべてXBOX360配列、またはPlayStation配列(押し込める2軸アナログ2本、十字キー、正面4ボタン、LR2組、START/SELECT)の構成のため、XInput(XBOX360)でいう各ボタンの呼称、PS系でいう各ボタンの呼称、そしてその位置にあるボタンを押したときにコントロールパネルで光ったボタン番号、という組み合わせで表記しています。

F510

DInputモードとXInputモードそれぞれにおいて、MODEボタン消灯時(MODE消)とMODEボタン点灯時(MODE点)の2モードでの割り当ての変化を見た。

物理位置(XInput) 右X軸 右Y軸 左X軸 左Y軸 十字
左右
十字
上下
A B X Y LB RB LT RT LS RS BACK START ガイド
物理位置(PS) × L1 R1 L2 R2 L3 R3 SELECT START PS
反応した

or ボタン番号
D/MODE消 Z軸 Z回転 X軸 Y軸 POV左右 POV上下 2 3 1 4 5 6 7 8 11 12 9 10 なし
D/MODE点 Z軸 Z回転 POV左右 POV上下 X軸 Y軸 2 3 1 4 5 6 7 8 11 12 9 10 なし
X/MODE消 X回転 Y回転 X軸 Y軸 POV左右 POV上下 1 2 3 4 5 6 Z+ Z- 9 10 7 8 ガイド
X/MODE点 X回転 Y回転 POV左右 POV上下 X軸 Y軸 1 2 3 4 5 6 Z+ Z- 9 10 7 8 ガイド

DInputモードでは右スティックはZ軸とZ回転にアサイン、XInputだとX回転とY回転にアサインされるということで、モードにより右スティックの扱いが違うという結果に。

LTとRTはDInputではデジタルボタンの扱いだが、XInputだとアナログトリガーになる。ただし、DirectInputのAPIからXInputモードの本機を見た場合、両方で同じZ軸を共有しているため、LTとRTの同時押しを認識できない(つねに両方の押し込み量の差がZ軸の値に表れる)XInputのAPIでは両方の押し込み量を個別に取得できるので同時押しでも拾える。DInputのゲームにXInputモードの本機を使う理由は皆無じゃなかろうか?

F710

F510同様、DInputとXInputそれぞれのモードでMODE点灯、消灯各状態での割り当ての変化を見た。

物理位置(XInput) 右X軸 右Y軸 左X軸 左Y軸 十字
左右
十字
上下
A B X Y LB RB LT RT LS RS BACK START ガイド
物理位置(PS) × L1 R1 L2 R2 L3 R3 SELECT START PS
反応した

or ボタン番号
D/MODE消 Z軸 Z回転 X軸 Y軸 POV左右 POV上下 2 3 1 4 5 6 7 8 11 12 9 10 なし
D/MODE点 Z軸 Z回転 POV左右 POV上下 X軸 Y軸 2 3 1 4 5 6 7 8 11 12 9 10 なし
X/MODE消 X回転 Y回転 X軸 Y軸 POV左右 POV上下 1 2 3 4 5 6 Z+ Z- 9 10 7 8 ガイド
X/MODE点 X回転 Y回転 POV左右 POV上下 X軸 Y軸 1 2 3 4 5 6 Z+ Z- 9 10 7 8 ガイド

F510と同じ結果に。F510無線版と言い切ってよさそう。

JC-U3613M

モードとしてはDInputとXInputモードの変更のみ。

物理位置(XInput) 右X軸 右Y軸 左X軸 左Y軸 十字
左右
十字
上下
A B X Y LB RB LT RT LS RS BACK START ガイド
物理位置(PS) × L1 R1 L2 R2 L3 R3 SELECT START PS
反応した

or ボタン番号
DInput Z軸 Z回転 X軸 Y軸 POV左右 POV上下 3 4 1 2 5 6 7 8 9 10 11 12 13
XInput X回転 Y回転 X軸 Y軸 POV左右 POV上下 1 2 3 4 5 6 Z+ Z- 9 10 7 8 ガイド

DInput時の配列をF510/710と比べてみると軸はMODE消灯時のそれと同じであるが、
上面パッドボタンの番号振りがX→Y→A→Bの順で、BACKとSTART、LSRSの順番がF510/710とあべこべ。
XInputオン時はF510/710とまったく同じ配列。

BSGP1204P

MODE赤=MODEボタンが赤色に光っている状態、MODE緑=MODEボタンが緑色に光っている状態。
MODEボタンを押すことで赤と緑交互にモードが切り替わる。

物理位置(XInput) 右X軸 右Y軸 左X軸 左Y軸 十字
左右
十字
上下
A B X Y LB RB LT RT LS RS BACK START ガイド
物理位置(PS) × L1 R1 L2 R2 L3 R3 SELECT START PS
反応した

or ボタン番号
MODE赤 Z軸 Z回転 X軸 Y軸 POV左右 POV上下 3 2 4 1 7 8 5 6 11 12 9 10 なし
MODE緑 なし なし なし なし X軸 Y軸 3 2 4 1 7 8 5 6 11 12 9 10 なし

MODE緑の時はアナログスティックが2本とも無効になる潔さが際立つ。上面ボタン配列はPSで言うと△→○→×→□の順で時計回りに割り当て。L2R2がボタン番号的に先になっているのも若干目立つ。

HORIPAD3 PRO

物理位置(XInput) 右X軸 右Y軸 左X軸 左Y軸 十字
左右
十字
上下
A B X Y LB RB LT RT LS RS BACK START ガイド
物理位置(PS) × L1 R1 L2 R2 L3 R3 SELECT START PS
反応した

or ボタン番号
Z軸 Z回転 X軸 Y軸 POV左右 POV上下 2 3 1 4 5 6 7 8 11 12 9 10 13

同じPS3対応のBSGP1204Pと比べるとアナログ軸は一致するものの上面ボタンに違いがある。□→×→○→△、XBOX360ならXABYという順で番号が振ってある。L1R1L2R2の並び、L3R3とSELECT/STARTのボタン順はLogicoolパッドのDInputモードのそれと同じ。ただしこちらはガイドボタンに相当するPSボタンもボタン13として使用可能になっている。

まとめ

右スティックについては横=Z軸、縦=Z回転でほぼ間違いなさそうなんですが(XInputパッドをDirectInputで読むのはLT,RTが使い物にならないのでアレとして)、ボタンは見事にバラバラな結果でして、やはりゲームパッドを推奨するならキーコンフィグは最優先で実装すべきといえるでしょう。

あと地味な点ですが、方向ボタン(十字キー)がPOVに固定されてるパッドが増えて来ているのも気になります。デジタル入力を拾う2DゲーなどではPOVの読み取りにも対応しておくのがユーザーにとって優しいんじゃないでしょうか。

*1:PAE有効にして8GBをRAMディスクに使用

.NET用ラバーバンドクラス

C#でツール作るたび毎回毎回ラバーバンドで苦労してる気がしてきたので、クラスにまとめて使い回そうともくろんでみたり。

説明とかめんどくさいのでソース見てください。動作サンプルもつけてます。VisualStudio2012用になってますが自力でソリューション作り直せばVS2010でも.NET 4.5以前でもイケるんじゃないでしょうか(少なくともライブラリの方は)。

ピクチャボックス pictureBox1 をフォーム Form1 に置いてるものとして、
下記のようなコードで動かします。

public partial class Form1 : Form
{
    DNASoftwares.Rubberband rub;

    public Form1()
    {
        InitializeComponent();
        //新しいインスタンスの作成。引数は矩形初期値、ハンドルをつける場所、可動範囲の順
        rub = new DNASoftwares.Rubberband(new Rectangle(100, 100, 250, 250),
            DNASoftwares.Rubberband.HandlePosition.All,
            new Rectangle(Point.Empty, pictureBox1.ClientSize));
        toolStripComboBox1.SelectedIndex = 1;
    }

    private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
    {
        //基本こんな感じで、if(メソッド) コンテナ.Refresh(); とすればいいです。
        //MouseDown イベントでは MouseDownEvent メソッドを呼びます
        if(rub.MouseDownEvent(e)) pictureBox1.Refresh();
    }

    private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
    {
        Cursor c;
        //MouseMove イベントでは MouseMoveEvent メソッドを呼びます
        if (rub.MouseMoveEvent(e, out c)) pictureBox1.Refresh();
        // MouseMoveEvent は ラバーバンド上でのカーソル変更も支援できます。
        pictureBox1.Cursor = c;
    }

    private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
    {
        //MouseUp イベントでは MouseUpEvent メソッドを呼びます
        if(rub.MouseUpEvent(e)) pictureBox1.Refresh();
    }

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        e.Graphics.FillRectangle(new SolidBrush(this.BackColor),e.ClipRectangle);
        //Paint イベントでは PaintEvent メソッドを呼びます。背景クリア等は各自。
        rub.PaintEvent(e);
    }

// 以下省略