DoGA-L3で「取り急ぎ」かっこいいモデルを作ってUnityで動かして遊ぶ

!ライセンスについてDoGA様から返答がありましたので追記しております。(2/6更新)

気がついたら前回の投稿から1年が経とうとしていて焦っているDNAです。Twitterでは18年前に作ったゲームをリメイクできないかなーとちょこちょこと進捗(してない)動画を上げていたりしましたが、その中で愛用しているDoGAさんの「PLAY Animation Importer for Unity」が昨年12月に更新されて、大変使い勝手がよくなったので、というか私の要望に応えてバージョンアップしてくださいましたので、お礼になるかわからないのですがDoGA-L3を改めて紹介してみたいと思います。

DoGA-L3とは

DoGA-L3はDoGA-LシリーズというCGアニメーション入門ツールの一つです。
Webサイト(CGA入門キット DOGA-L シリーズ)の紹介によれば、

DOGA-Lシリーズは、まったく初心者の方でも、 手軽にCGアニメーション (以下CGAに略す) の楽しさを体験していただくことを目的に開発された、CGA入門システムです。

……だそうです。実際機能もかなり絞られていますが、3DCGアニメを制作するのに最低限必要な機能は揃っています。

f:id:dnasoftwares:20170129002223g:plain
たくさんある半完成のパーツを組み合わせてモデルを作り……

f:id:dnasoftwares:20170129002318p:plain
モデルを配置してシーンやアニメーションを作ります。

f:id:dnasoftwares:20170129002404j:plain
Direct3Dでリアルタイムプレビューできるほか専用レンダラーで高画質出力もできます。

f:id:dnasoftwares:20170129002759p:plain:w300 f:id:dnasoftwares:20170129002935p:plain:w300 f:id:dnasoftwares:20170129002937p:plain:w300 f:id:dnasoftwares:20170129002938p:plain:w300
パーツはカテゴリごとに整理されており総数はそこそこあります。発想次第で色々作れます。
元々15年以上前のツールなだけに、そのころのスペックに合わせたポリゴン予算でパーツが作られています。どのパーツも良い感じのローポリ感があります。
プラモデルで既製品のパーツを借りてディテールアップしたり別のモデルを作ることをキットバッシング(Kitbashing)というそうですが、まさにキットバッシングを3DCGでやるツールといえるでしょう。

で、このモデルを作る部分(パーツアセンブラ)のデータをUnityにインポートできるようにするのがDoGA謹製のPLAY Animation Importer for Unityになります。

DoGA-L3他、系列の各製品のライセンスを購入すると各製品のパーツアセンブラで製作したモデルデータをUnityにインポートできるようになります。ちょっとクセのある出力になっていますが、他のエディタ拡張アセットと組み合わせたりなんだりで綺麗にまとめることができますので、併せて紹介したいと思います。

本稿では画面写真や操作はDoGA-L3を前提に進めます。(DNAが持ってるのがDoGA-L3のライセンスなので……)本当はDoGA-L1,L2,L3とステップアップする設計なんですが、Unityが扱える方ならDoGA-L3もすぐ慣れるでしょう。詳しい説明はしませんのでDoGAのサイト等を見ながら進めてください。分からないと思った場合はより初心者向けのとてかんCGで進めてもいいかもしれません。いずれにしろシェアウェアなので試用して問題なければライセンスを購入してください。
なお、通常のライセンスは非商用利用に限っての許諾となっており、商用、対価を取る用途には使えません。
ただし、DoGA-L3に限り商用利用可能なプロフェッショナルライセンスが設定されているほか、同人ゲームにおいてはパブリッシャーを通さない範囲での頒布は通常ライセンスの購入で許諾されるとのことです。パブリッシャーを通しての販売を行う場合には別途お問い合わせくださいとのことでした。(DoGA様ご返答ありがとうございました。)
DoGAの掲示板ではインポーターを使ってインポートしたモデルの使用について見解が示されています。

http://doga.jp/2010/reading/diary/201407.html#20140730kama
> このプラグインによって、L3等の形状データをUnityで自作の
> ゲームに利用するだけでなく、L3等で作ったデータを
> Unity アセットストアで販売することが可能になります。

日記でかまださんがこう書いている通り、
商用ライセンスは不要でUnityアセットストアでの販売が可能です。

これまでも L シリーズのデータを使って同人ソフトの開発・販売は認めてきましたがその方針を維持しています。
(明らかに同人の規模を超える、また商業ベースだと商用ライセンスの導入をお願いしています)

ということで取り急ぎ作ったモデルに愛着が沸いたらそのまま完成まで使いましょう。

試す

前述の通りUnityへのインポートの条件として製品の購入が必要ですが、購入前にとりあえずインストールして手触りを確かめるのがいいでしょう。
CGA入門キット DOGA-L シリーズのページの「DoGA-L3」内「ダウンロード」でダウンロードページに進みます。お目当てはモデリング部分であり、動画を出力するわけではないので「新規インストール 最小限」のEXE一つで大丈夫。
バージョンが2008とかなっててちょっとびっくりしますが、Windows10でも特に問題なく動作するはずです。何か挙動不審があったらDOGA-L3 βから2010年のβ版も入れるといいかもしれません。

動作に問題が無ければライセンスを購入して登録してください。

とりあえずモデル作る

というわけでモデルを作ります。パーツアセンブラでもっそもっそパーツを盛りましょう。色・テクスチャもインポーターは読み取ってくれるので気兼ねなく。ただし表面材質の設定は追従しない(すべてLegacy Diffuseでインポートされる)ので、ここはインポート後にどうにかします(次の記事で書きます)。

f:id:dnasoftwares:20170129194802p:plain
ここでは弊サークルで以前作ったモデルを使います。上のGIFアニメで出てるやつですね。いまちびちびリメイクしようとしてるの自機(のデータがなくなったので、当時のレンダリング画像を見て再度作ったもの)です。ちょうどUnityインポートで引っかかる色々な要素が混ざってるので好適かなと。

モデルの保存時など特別なことをする必要はなく、普通に保存して問題ありません。DoGAのデータファイルの場所は、標準では「(DoGA-L3のインストールフォルダ)\data」です。ファイルセレクタではどのフォルダでも指定できますので、たとえばUnityプロジェクトのフォルダにしてまとめてバージョン管理下に置くことも一応できます。

Unityのプロジェクトを仕込む

ここでは新規のプロジェクトを作ります。もちろんインポート先のプロジェクトがすでにあるならそっちを開いていいでしょう。
DoGAPLAY Animation Importer for Unityから、最新版のunitypackage(20161223)をダウンロードしてインポートします。
Asset Storeにもあるのですが、こちらはまだバージョンが上がってないようです。

インポートとコンパイルが終わったら「Window」メニューに「PLAY Animation (TotekanCG) Importer」が増えていることと、それを選んだら開くインポーターのウィンドウで
「Installed DoGA Products」の欄に登録したツールが表示されていることを確認してください。
f:id:dnasoftwares:20170129201026p:plain
f:id:dnasoftwares:20170129201139p:plain

インポートしてみる

エクスプローラDoGAのデータフォルダを開けます。中にある拡張子.l3pのファイルをUnityのインポーターのウィンドウにドラッグ&ドロvップ。「Import tasks」リストにファイルが追加されるはずです。
f:id:dnasoftwares:20170206004936p:plain
Scaleの値がやたら小さかったりするのが気になるかもしれませんが、あまり気にせず「Import All」でインポートを開始してしまいましょう。材質の多いモデルだと10~20秒程度時間がかかります。オブジェクトはシーンの原点に生成されます。
f:id:dnasoftwares:20170206005008p:plain

また、モデルのPrefabがAssetsフォルダの直下に生成されます。ただし、このモデルには極めて変なクセがついています。後述。

使う

折角飛行機のモデルなんですから飛ばしてみましょう。毎度おなじみテラシュールブログにちょうどいい記事がありますので参考にします。
tsubakit1.hateblo.jp

基本的にはこの記事通りに進行すればモデルを空に飛ばせます。ただし上の記事通りに作業を進めようとすると数点引っかかると思います。

メッシュがマテリアル単位で別のGameObjectに分割されている

何かしらのメッシュ統合アセットを使う必要があります。メッシュコライダの作成がうまくいかないと思いますのでとりあえず近似のBoxやCylinderなどを組み合わせましょう。

謎の回転と拡大がついている

角度は親も子も全部0にすれば元通り、拡大も親子とも1倍に戻せば元通りになります。割と謎。

両方設定して適当に床を置いてモデル位置を調整、カメラは手抜きですがとりあえず飛行機の子にして勝手に追従させましょう。
背景が寂しいのでMaruchuさんのTekitouCityGeneratorを使います。
many.chu.jp

パラメータをわちゃわちゃ調整しつつできあがったのがこちら。

とてかんCGインポーターで作ったモデルを飛ばす(1)

とりあえずとしてはこれでいいんですが、もうちょっと色々できますので以下次号。

Projectorでデカールもどき

突然地味なUnityネタが沸いてくるD.N.A.のおぼえがきですいかがお過ごしでしょうか。

※今回の記事では利用例のテクスチャにハムコロ様STG素材を使用しています。

いきさつ

STGの試作を延々続けておりますが、地味に困ってるのが「Terrainの上の地上物の破壊跡の処理」です。
板ポリゴンにテクスチャを張っただけでは、Terrainにめり込んだりして見た目がよろしくありません。

f:id:dnasoftwares:20160221182338p:plain

そういうときはDecal(オブジェクトの表面に沿って別のテクスチャを張る)を使うのですが、
Decalを実現できるアセットというといくつかあるものの……

と、やることに対して$がかかりすぎる感じだったり微妙な物足りなさがあったり……と悩んでいたのですが、
良く考えればUnity標準機能のProjectorがあるじゃない、ということでProjectorでなんとかしてみようと試みたのでした。

シェーダーいじり

Standard Assetで入手できるProjector用のシェーダーは、

  • Multiply
  • Light

の2つだけです。MultiplyもLightもテクスチャの色をそのまま貼り付けられないので、これらを元にテクスチャの色をブレンドせず貼り付けられるシェーダーを書きます。以下、ProjectorLight.shaderを元に改造したコード。

リスト ProjectorDecal.shader

Shader "Projector/Decal" {
	Properties {
		_Color("Main Color", Color) = (1,1,1,1)
		_ShadowTex ("Cookie", 2D) = "gray" {}
	}
	Subshader {
		Tags {"Queue"="Transparent"}
		Pass {
			ZWrite Off
			ColorMask RGB
			Blend SrcAlpha OneMinusSrcAlpha //ブレンドを通常のアルファブレンドに
			Offset -1, -1

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			#include "UnityCG.cginc"
			
			struct v2f {
				float4 uvShadow : TEXCOORD0;
				UNITY_FOG_COORDS(2)
				float4 pos : SV_POSITION;
			};
			
			float4x4 _Projector;
			float4x4 _ProjectorClip;
			
			v2f vert (float4 vertex : POSITION)
			{
				v2f o;
				o.pos = mul (UNITY_MATRIX_MVP, vertex);
				o.uvShadow = mul (_Projector, vertex);
				UNITY_TRANSFER_FOG(o,o.pos);
				return o;
			}

			fixed4 _Color;
			sampler2D _ShadowTex;
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 texS = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
				texS.rgb *= _Color.rgb;
				// texS.a = 1.0-texS.a; //デカールの場合はアルファ値はそのままでいいので削除

				fixed4 res = texS;

				UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(1,1,1,1));
				return res;
			}
			ENDCG
		}
	}
}

いじったのは

  • FallOffの設定はいらないのでパラメータごとバッサリ削除。
  • Blendをアルファブレンドに。
  • アルファ値を反転する処理がいらないので削除(このままだと透明と不透明が反転してしまう)

の3点。これでうまくいきました。

配置してみる

実際にシーンに置いてみます。Projectorを配置して地面に向け、下記のようにプロパティを設定。

f:id:dnasoftwares:20160221182426p:plain

Orthographicを有効にして地面との距離によってデカールが変化しないようにします。Materialには先ほどのシェーダを指定したマテリアルを用意して割り当て。Cookieとして破壊跡の絵を用意。Ignore Layersで背景以外全部を指定し変なところに跡がつかないように。テクスチャの方もWrap Modeを「Clamp」にするのを忘れずに。

実際にはこんな感じになります。
f:id:dnasoftwares:20160221182953p:plain

ちゃんとTerrainに沿っていることがわかります。
f:id:dnasoftwares:20160221183002p:plain

まとめ

簡易的なデカール表現としてはこれでいいでしょう。ただ、板ポリならShader次第でなんでもありだった凝った表現ができないのと、ライティングを一切考慮していないままなので、当然ライティングに一切追従せず、使いどころは極めて限定されます。やっておいてなんですが基本は板ポリでしのぎつつ、必要なところだけProjectorにするといった方がいいでしょう。

あと、誰かTerrainにDecal張れるアセットご存じでしたら教えてください。(Asset Storeの説明文だとそのへんがわからないヤツが多くて突撃しづらい……)

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ヶ月に何やってたのかをまとめて振り返ってみたいなと。数日間ダラダラと所感を書いてみる予定。

続きを読む