3
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

MagicaVoxel→Blender→Unityの流れで変形メカを作る

unityroom でデモ版を公開している自作ゲームの敵キャラの箱、その変形パターンの検討を含めたデザインを MagicaVoxel で作成し、Blender を通して Unity に持っていくことができましたので、その手順を書き起こしてみたいと思います。(誰得な内容とは思いますが、主に明日以降、箱を量産する自分のため…orz)

Magica Voxel

まず単色の立方体を表示し、パーツに分ける所を別の色で塗っていきます。その後、塗り分けたパーツが外れた時の様子を描き、さらにその内側を塗り分けていきます。
変形パターンの各ステップを隣に配置しておくと変形の流れが把握し易いです。
この画像では、もっとたくさんの変形ステップを用意するつもりでしたが、3つ目で早々にネタ切れになりましたorz
(あとなぜか水筒の飲み口の設計をしているような気分になった事はここだけの秘密…)
01MagicVoxel.png

一通りできたら箱に戻してエクスポートしていきます。
書き出したい色を残して他を削除し、OBJ 形式で保存していきます。

Blender

ここから Blender の作業になります。

インポートとベベル

File/Import で OBJ ファイルを取り込みます。

blender-01Imported-obj.png

このままCtrl+Bでベベルを付けると写真のように恐ろしいことになります。

blender-02bevel-invalid.png

ベベルは同一平面上の分割線にもオフセットをつけてしまう、また頂点上の法線ベクトルがオフセットの方向に関係してしまうようですので、
ベベルをかける前に、以下をやっておいた方が良いです。

  • EditMode で全選択(A)の後、Mesh/Vertices/Remove Doubles で頂点の統合
  • Mesh/Faces/Tris to Quads でできるだけ四角ポリゴンに統合

blender-03tristoquads.png

また、鋭角な三角ポリゴンが残っているとベベルの結果がおかしくなる傾向にあるようなので、面の統合(Mesh/Delete/Dissolve Edges)とKnifeツールを使って直しておきます。

blender-04quadsfixed.png

この修正後のモデルについては Ctrl+B でベベルをかけても、以下のように正しく反映されます。

blender-05bebel.png

ボーンを仕込む

簡単な構造ですがパーツごとにボーンを仕込んでおきます。

Blender-Armature.png

Blender-Armature-Hierarchy.png

アニメーション

フックが開く×2、安全装置が開くという簡単なアニメーションですが個々のActionとして作成しておきます。(この画像では開いた後の状態も1フレームのActionにしていましたが、最終的にこれは不要になりました)

Blender-Animation-Actions.png

ロックオンターゲットを仕込む

ロックオンターゲットの場所に Add/Empty/Plain Axis を仕込んでおきます。その際、特殊な名称(LockonTarget_01_01_01等)にしておきます。このオブジェクト名は Unity にインポートした後も残るので、Unity のスクリプト側で処理しやすい形式にしておきます。書式は以下のように決めました。

LockonTarget_[状態番号]_[グループ番号]_[ターゲット番号]

同一グループ番号のターゲットが破壊されるとそのグループのギミックが起動し、全てのグループのギミックが動くと状態が次に遷移する、という形にしています。

まず左右2つあるフックの根本にターゲットを配置します。
Blender-LockOnTarget-State01.png

次に安全装置の接点に多数のターゲットを配置します。
Blender-LockOnTarget-State02.png

最後にコアとなる場所にターゲットを配置します。
Blender-LockOnTarget-State03.png

それぞれ↑の書式に従う名前を付けています。

Blender-LockOnTarget-State01-Hierarchy.png
Blender-LockOnTarget-State02-Hierarchy.png
Blender-LockOnTarget-State03-Hierarchy.png

FBX でエクスポート

最後に FBX 形式でエクスポートします。エクスポート時の設定は以下の通りです。

Main タブ
- Camera/Lamp/Otherは不要
- Model の Empty がチェックされているかどうかチェック

Geometry タブ
- Apply Modifiers, Use Modifiers Renderer Setting にチェック

Armature タブ
- Only Deform Bones にチェック
- Add Leaf Bones のチェックははずす

Animation タブ
- Baked Animation, All Actions にチェック
- Key All Bones, NLA Strips, Force Start/End Keying ははずす

Unity

ようやくここから Unity の作業になります。

FBXファイルをインポート

ドラッグ&ドロップで FBX ファイルを取り込むと以下のようにアセットとして追加されます。
ここでボーンやアニメーションがちゃんと出ているかどうかを確認しておきます。

Unity-01fbx-imported.png

AnimatorController(状態遷移)の作成

次に、アニメーションの状態遷移を作成します。ポイントとしては、ギミックごとに Layer に分けておきます。2つあるフックはどちらが先に起動するかはわからないため、1つのLayerに押し込むことができません。

その分、個々の状態遷移は基本的にアイドル状態から開くモーションに移るだけ、という単純なものになります。

こちらは1つめのフックの状態遷移図です。
Unity-02animator-hook1.png

続いて2つめのフックの状態遷移図。
Unity-02animator-hook2.png

最後に安全装置の状態遷移図。
Unity-02animator-baselayer.png

1つ目のフックの状態遷移図について、それぞれの状態のインスペクタは以下のようになります。
アイドル状態では AnimationClip を設定しません。
Unity-04animator-idle.png

開く状態の時はFBXに含まれる AnimationClipを指定しておきます。
Unity-04animator-hook1-open.png

Transition では、2つの状態のアニメーションが重なる必要がなく即座に次に移るように設定しておきます。
Unity-03animator-transition.png

スクリプトからの制御

初期化時にロックオンターゲットを拾い出す

Linq to GameObject アセットを使うと以下のように Descendants() 一発で子孫のGameObjectが取得できるので、そのうち name が↑でかいた書式のものを Where() で抽出することができます。

BoxController.cs
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Linq.Expressions;
using UnityEngine;
using Unity.Linq;
    :
public class BoxController : MonoBehaviour, ITargetable {
    :
    const string prefixLockOnTarget = "LockOnTarget";
    Regex regExLockOnTarget = new Regex($"{prefixLockOnTarget}_(\\d\\d)_(\\d\\d)_(\\d\\d)");
    List<GameObject> targetPositions;
            :
    void Construct() {
        this.targetPositions = this.gameObject.Descendants().Where(x => {
            return x.name.StartsWith(prefixLockOnTarget);
        }).ToList();
    }
}

状態初期化時にターゲットオブジェクトの作成

各状態の初期化処理は以下のようになります。↑で作成したtargetPositionsから、現在の状態に対応するターゲットを取り出してターゲットオブジェクトの出現処理を行い、this.targetsCurrent に登録しています。
同時に、現状態のパーツ一覧と要破壊ターゲット数を this.partState に登録します。
この this.targetsCurrent のサイズが 0 の時は、要破壊オブジェクトがなくなった事になるので、この箱そのものを破壊する処理に入ります。

BoxController.cs
public class BoxController : MonoBehaviour, ITargetable {
        :
    List<TargetController> targetsCurrent = new List<TargetController>();
    int currentState = 1;
    Dictionary<int, int> partState = new Dictionary<int, int>();
        :
    public void initTargets()
    {
        this.partState.Clear();
        this.targetsCurrent = this.targetPositions.Where(x => x.name.StartsWith($"{prefixLockOnTarget}_{this.currentState:00}_")).Select(x => {
            var target = this.gameMain.spawnTarget(x.transform.position, x.transform.rotation, this.targetSettings.lifeTarget0Default, x.name, this);
            var match = regExLockOnTarget.Match(x.name);
            if (match.Success && match.Groups.Count == 4) {
                var groups = match.Groups;
                var indexState = int.Parse(groups[1].Captures[0].Value);
                var indexPart = int.Parse(groups[2].Captures[0].Value);
                var indexTarget = int.Parse(groups[3].Captures[0].Value);
                Debug.Log($"indexState={indexState}, indexPart={indexPart}, indexTarget={indexTarget}");
                if (!this.partState.ContainsKey(indexPart)) {
                    this.partState.Add(indexPart, 1);
                } else {
                    this.partState[indexPart]++;
                }
            }
            return target;
        }).ToList();

        if (this.targetsCurrent.Count == 0) {
            Debug.Log("Box destrpyed!");
            this.gameMain.spawnExplosion(this.transform.position);
        }
    }
}

ターゲットの破壊時は onTargetDestroyed() が呼ばれるようにしています。
この時、破壊されたターゲットのオブジェクト名から状態番号・パーツ番号を取得し、this.partState を更新します。同一パーツ内のターゲットが全て破壊されたら、該当するアニメーションを起動します。

BoxController.cs
public class BoxController : MonoBehaviour, ITargetable {
        :
    public void onTargetDestroyed(TargetController target)
    {
        var match = this.regExLockOnTarget.Match(target.gameObject.name);
        if (match.Success && match.Groups.Count == 4) {
            var groups = match.Groups;
            var indexState = int.Parse(groups[1].Captures[0].Value);
            var indexPart = int.Parse(groups[2].Captures[0].Value);
            if (this.partState.ContainsKey(indexPart)) {
                this.partState[indexPart]--;
                if (this.partState[indexPart] == 0) {
                    var key = new System.ValueTuple<int, int>(this.currentState, indexPart);
                    if (this.partNameMap.ContainsKey(key)) {
                        Debug.Log($"currentState={this.currentState}, indexPart={indexPart}, trigger={this.partNameMap[key]}");
                        this.animator.SetBool(this.partNameMap[key], true);
                    }
                }
            }
        }
        this.targetsCurrent.Remove(target);
        if (this.targetsCurrent.Count == 0) {
//            this.animator.SetBool($"Step{this.currentState:00}Destroyed", true);
            this.currentState++;
            this.initTargets();
        }
    }
}

こんなん作っています

ということで解説は以上となります。
開発中のゲームのデモ版を unityroom にて公開中です。よろしければ遊んでみて下さい。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?