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);
    }

// 以下省略