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