unityroom でデモ版を公開している自作ゲームの敵キャラの箱、その変形パターンの検討を含めたデザインを MagicaVoxel で作成し、Blender を通して Unity に持っていくことができましたので、その手順を書き起こしてみたいと思います。(誰得な内容とは思いますが、主に明日以降、箱を量産する自分のため…orz)
#Magica Voxel
まず単色の立方体を表示し、パーツに分ける所を別の色で塗っていきます。その後、塗り分けたパーツが外れた時の様子を描き、さらにその内側を塗り分けていきます。
変形パターンの各ステップを隣に配置しておくと変形の流れが把握し易いです。
この画像では、もっとたくさんの変形ステップを用意するつもりでしたが、3つ目で早々にネタ切れになりましたorz
(あとなぜか水筒の飲み口の設計をしているような気分になった事はここだけの秘密…)
一通りできたら箱に戻してエクスポートしていきます。
書き出したい色を残して他を削除し、OBJ 形式で保存していきます。
#Blender
ここから Blender の作業になります。
インポートとベベル
File/Import で OBJ ファイルを取り込みます。
このままCtrl+Bでベベルを付けると写真のように恐ろしいことになります。
ベベルは同一平面上の分割線にもオフセットをつけてしまう、また頂点上の法線ベクトルがオフセットの方向に関係してしまうようですので、
ベベルをかける前に、以下をやっておいた方が良いです。
- EditMode で全選択(A)の後、Mesh/Vertices/Remove Doubles で頂点の統合
- Mesh/Faces/Tris to Quads でできるだけ四角ポリゴンに統合
また、鋭角な三角ポリゴンが残っているとベベルの結果がおかしくなる傾向にあるようなので、面の統合(Mesh/Delete/Dissolve Edges)とKnifeツールを使って直しておきます。
この修正後のモデルについては Ctrl+B でベベルをかけても、以下のように正しく反映されます。
ボーンを仕込む
簡単な構造ですがパーツごとにボーンを仕込んでおきます。
アニメーション
フックが開く×2、安全装置が開くという簡単なアニメーションですが個々のActionとして作成しておきます。(この画像では開いた後の状態も1フレームのActionにしていましたが、最終的にこれは不要になりました)
ロックオンターゲットを仕込む
ロックオンターゲットの場所に Add/Empty/Plain Axis を仕込んでおきます。その際、特殊な名称(LockonTarget_01_01_01等)にしておきます。このオブジェクト名は Unity にインポートした後も残るので、Unity のスクリプト側で処理しやすい形式にしておきます。書式は以下のように決めました。
LockonTarget_[状態番号]_[グループ番号]_[ターゲット番号]
同一グループ番号のターゲットが破壊されるとそのグループのギミックが起動し、全てのグループのギミックが動くと状態が次に遷移する、という形にしています。
それぞれ↑の書式に従う名前を付けています。
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 ファイルを取り込むと以下のようにアセットとして追加されます。
ここでボーンやアニメーションがちゃんと出ているかどうかを確認しておきます。
AnimatorController(状態遷移)の作成
次に、アニメーションの状態遷移を作成します。ポイントとしては、ギミックごとに Layer に分けておきます。2つあるフックはどちらが先に起動するかはわからないため、1つのLayerに押し込むことができません。
その分、個々の状態遷移は基本的にアイドル状態から開くモーションに移るだけ、という単純なものになります。
1つ目のフックの状態遷移図について、それぞれの状態のインスペクタは以下のようになります。
アイドル状態では AnimationClip を設定しません。
開く状態の時はFBXに含まれる AnimationClipを指定しておきます。
Transition では、2つの状態のアニメーションが重なる必要がなく即座に次に移るように設定しておきます。
スクリプトからの制御
初期化時にロックオンターゲットを拾い出す
Linq to GameObject アセットを使うと以下のように Descendants() 一発で子孫のGameObjectが取得できるので、そのうち name が↑でかいた書式のものを Where() で抽出することができます。
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 の時は、要破壊オブジェクトがなくなった事になるので、この箱そのものを破壊する処理に入ります。
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 を更新します。同一パーツ内のターゲットが全て破壊されたら、該当するアニメーションを起動します。
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 にて公開中です。よろしければ遊んでみて下さい。