概要
最近、ネトフリで鋼の錬金術師を見ました。国家錬金術師達がそれぞれ特徴的な錬成を行っていてとてもかっこいいです。
そこで自分でも錬金術を出したいと思い、Oculus Quest用アプリを作ってみました。
作ったもの
apkをこちらから配布します。
Quest2にも対応しているはずなので是非遊んでみてください!
発動できる錬金術は
マスタング大佐の指パッチン炎錬成!
参照
エドの地面錬成
参照
の2つです。
地面錬成を敵のゾンビに当てると倒すことが出来ます
作り方
要点だけの結構ざっくりした説明なので抜けているところも多いです。ご了承ください
以下の4ステップに分けて説明していきます
- シーン
- 炎錬成
- 地面錬成
- 敵
0. 開発環境
以下の構成で開発しました。
- Unity 2019.3.9f1
- 初代Oculus Quest
1. シーン
まずは世界を創造します
1.1 Quest対応
Quest対応アプリを作る際の設定はこりんさんのブログが参考にしました。細かくまとめて頂いてありがとうございます。
詳しい説明はそちらのブログをご参照ください。
今回はOculus Integration v20.1の以下フォルダをインポートしました。
OVRCameraRigにアタッチされているOVRManagerの設定は以下の3つを確認すればOKだと思います
以上でQuestでハンドトラッキングをする設定は完了です。
今回は手を自然な見た目にするために肌色のマテリアルを
Oculus Linkをして実行すればビルド不要で確認出来ます。
1.2 フィールド作成
フィールドにはこちらのアセットを使用しました。
このように何やら楽しげなフィールドを作成する素材がいっぱい入っていて便利です
2. 炎錬成
マスタング大佐になる機能を追加しましょう
2.1 エフェクト作成
炎のエフェクトはこちらのサイトを参考にして作成しました
今回は小さめの炎が一気に燃え上がって1秒で消えるようにしました
2.2 指パッチン判定
指パッチンは
- 中指を曲げようとする力を親指で止める
- 親指を外す
- 中指が手のひらに当たる
ことで音を出します。
どうやら、中指が曲がったイベントを取得できれば良さそうです。
ただ、それだけだと握りこぶしを作ったときにも反応してしまうため。「人差し指が伸びていること」も条件に加えます。
指が伸びていることの判定はこちらの記事を参考にしました。
炎を出すアップデート関数はこの様になりました
private void Update()
{
// ハンドトラッキングをしていない、またはハンドトラッキングの信用度が低ければ誤作動を防ぐために無効にする
if(!_oVRHand.IsTracked || _oVRHand.HandConfidence.Equals(OVRHand.TrackingConfidence.Low)) return;
// 中指と人差し指の曲げ状況を取得する
// 曲げ閾値(_threshold)は0.27が丁度良かった
var isMiddleStraight = IsStraight(_threshold, OVRSkeleton.BoneId.Hand_Middle1, OVRSkeleton.BoneId.Hand_Middle2, OVRSkeleton.BoneId.Hand_Middle3, OVRSkeleton.BoneId.Hand_MiddleTip);
var isIndexStraight = IsStraight(_threshold, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
// 前回のフレームで中指が伸びていて今回のフレームで中指が曲がっていれば中指を曲げた瞬間だよね
// そのときに人差し指が伸びていれば指パッチンをしたよね
if(!isMiddleStraight && _isMiddleStraight_old && isIndexStraight)
{
// プレハブ化しておいた炎のエフェクトを生成する
// 生成場所は人差し指の先にするとそれっぽい
Instantiate(
_fire_base,
position: _oVRSkeleton.Bones[(int)OVRSkeleton.BoneId.Hand_IndexTip].Transform.position,
rotation: Quaternion.identity
);
}
_isMiddleStraight_old = isMiddleStraight;
}
こんな風に炎を出せるようになりました。マスタング大佐みたいでかっこいい!
3. 地面錬成
続いてエドになる機能を追加しましょう。今回作成する地面錬成は原作と完全に同じではありません。僕がかっこいいと思う&実装が簡単な様にカスタムしました
3.1 合掌したら手の甲に錬成陣を出す
エドが地面錬成をする前は両手を胸の前で合わせます。錬成の準備に必要なんでしょうか。
両手を合わせたことが分かりやすいように手の甲に錬成陣を出すことにします
3.1.1 エフェクト作成
手の甲に出す錬成陣はこちらのアセットを使用しました。
プレハブとして2つの魔法陣が含まれていますが、それは手の甲に出すには派手すぎたので画像とマテリアルを流用してこの様なものを自作しました。
内訳は以下の通りです
- Rune
- 中の模様。1つだけ生成し、左回転させる。黄色からオレンジに変化させる。
- Circle
- 外の円。1つだけ生成し、右回転させる。黄色からオレンジに変化させる。
- Pointlight
- 錬成陣の範囲を照らす黄色い光
これを両手の甲に配置し、手首ボーンの子としました。
最初から表示されているとおかしいので非アクティブにしてあります
錬成エネルギー?は数秒で無くなるという設定を勝手に考えたのでこの錬成陣はアクティブ化から3秒後に非アクティブ化するように設定しました
3.1.2 合掌判定
手を合わせたことを判定するために両手の手首ボーンにコライダーをこの様に配置しました。
物理的に干渉する必要は無いためトリガー設定をONにしています。
また、手の違いを判別できるようにコライダーを付けたオブジェクトのタグをそれぞれLeftHand
,RightHand
としました。
左手にアタッチしたスクリプトのOnTrigger関数で錬成陣をアクティブにします
private void OnTriggerEnter(Collider other)
{
// _anotherHandTagは"RightHand"
// コライダーが衝突した相手が右手なら
if(other.tag.Equals(_anotherHandTag))
{
//両手の甲の魔法陣を有効化する
foreach (var magicCircleOnHands in _magicCirclesOnHands)
{
magicCircleOnHands.SetActive(true);
}
}
}
これで手を合わせたときに手の甲に錬成陣が出るようになりました
3.2 地面に手を置いたら地面錬成をする
手の甲に錬成陣が出ている状態で地面に手を付くと地面錬成が出るようにします
3.2.1 エフェクト作成
地面錬成時には以下の2つのエフェクトを出します
- 茶色の錬成陣
- 地面っぽいので茶色です
- 手の甲に出した錬成陣の茶色バージョンです
- 地面隆起
- こちらのアセットを使用しました。
3.2.2 床に手を置いた判定
手の接地判定には3.1.2で作成した手の当たり判定を流用します。正しくやるなら両手が地面についたことを判定すべきですが、エドになりきった人ならちゃんと両手を同時についてくれると信じて左手のみの接地判定を行います。
まず、床のタグをFloor
に変更しておきます。
そして3.1.2で作成したスクリプトのOnTriggerEnter関数に地面錬成陣の生成処理を追加します。
private void OnTriggerEnter(Collider other)
{
// _anotherHandTagは"RightHand"
// コライダーが衝突した相手が右手なら
if(other.tag.Equals(_anotherHandTag))
{
//両手の甲の魔法陣を有効化する
foreach (var magicCircleOnHands in _magicCirclesOnHands)
{
magicCircleOnHands.SetActive(true);
}
}
// コライダーが衝突した相手が床なら
else if(other.tag.Equals("Floor"))
{
// 左手の錬成陣がアクティブなら
if(!_magicCirclesOnHands[0].activeSelf) return;
// 地面に魔法陣を出す
// _magicCircle_onGround_generateTransはプレイヤー前方1mに配置したTransform
Instantiate(
_magicCircle_onGround,
_magicCircle_onGround_generateTrans.position,
_magicCircle_onGround_generateTrans.rotation
);
// 地面錬成には錬成エネルギー?を使うので両手の錬成陣を非アクティブにする
foreach (var magicCircleOnHands in _magicCirclesOnHands)
{
magicCircleOnHands.SetActive(false);
}
}
}
これで地面錬成ができるようになりました。エドみたいでかっこいい!
4. 敵
せっかくなのでこれまで実装した錬成で敵を倒したい!ということでやっていきます
4.1 プレハブ作成
敵のプレハブにはこちらのアセットを使用しました。可愛いゾンビです。僕のお気に入り。
4.2 プレイヤーを追いかける機能を追加
プレイヤーを自然に追いかける機能の実装にはUnity組み込みのナビゲーションシステムが便利です
ナビメッシュをこの様にベイクしました。敵は水色の領域を移動できます。
敵プレハブにはNavMeshAgent
コンポーネントをアタッチします
敵にアタッチしたスクリプトのOnEnable
、Update
関数はこの様にしました
void OnEnable()
{
_navMeshAgent = GetComponent<NavMeshAgent>();
_animator = GetComponent<Animator>();
// _navMeshAgentの速度の値の5倍速でゾンビの歩行モーションを再生すると丁度いい見た目になる
_animator.SetFloat("MoveSpeed", _navMeshAgent.speed * 5);
}
void Update()
{
// プレイヤーの位置は移動しうるので目的地(プレイヤーの位置)を毎フレーム更新する
_navMeshAgent.destination = _playerTrans.position;
}
これで敵がプレイヤーににじり寄っていくようになりました!
4.3 攻撃判定
敵がプレイヤーに辿り着いたら攻撃するようにします
ゾンビの攻撃モーションに合わせてコライダーを作成します。物理的に干渉すると困るのでトリガー設定はONです
プレイヤー側の当たり判定はこの様にしました。半径0.5m、高さ2mのカプセルコライダーを位置のみHMDに追従する様にしてあります。
ゾンビがめり込んできたら困るのでトリガー設定はOFFです。
このオブジェクトのタグはPlayer
にしました。
最後に、敵にアタッチしたスクリプトのOnTriggerEnter
関数をこの様にします。
private void OnTriggerStay(Collider other)
{
// 当たった対象がプレイヤーじゃないまたは自分が攻撃できる状態でなければ何もしない
if(!other.CompareTag("Player") || !_canAttack) return;
// 攻撃モーションを再生する
_animator.SetTrigger("Attack");
// 攻撃後はクールタイムを挟む為、攻撃不能状態にする
_canAttack = false;
}
// 攻撃モーションのAnimation Eventに設定する関数
// _attackSpan秒の攻撃間隔を設ける
public IEnumerator CoAttackCoolTime()
{
yield return new WaitForSeconds(_attackSpan);
_canAttack = true;
}
これでプレイヤーに辿り着いた敵が攻撃モーションを行う様になりました!臨場感が出ますね。
4.4 倒される機能を追加
敵が地面錬成に当たったら倒れる様にします。
地面錬成にボックスコライダーで当たり判定を付けます。敵を吹き飛ばしたいわけでは無いのでトリガー設定はONです。
パーティクルシステムのトリガー設定を試してみましたが、いまいち使い勝手が悪かったのでこの手法にしました
このエフェクトにアタッチしたスクリプトのOnTriggerEnter
関数はこのようにしました。
衝突した敵のKill
関数を呼ぶだけです
private void OnTriggerEnter(Collider other)
{
if(!other.tag.Equals("Enemy")) return;
Enemy enemy = other.GetComponent<Enemy>();
if(!enemy) return;
enemy.Kill();
}
改めて考えると敵側で当たったことを検知した方が楽かもしれません
敵側のKill
関数はこのようにしました。
public void Kill()
{
// 死んでいるなら死なせない
if(_isDead) return;
_isDead = true;
// 死亡モーションを再生する
_animator.SetTrigger("Dead");
// これをしないと死亡モーション中にも移動してしまう
_navMeshAgent.isStopped = true;
}
死体が残っていたらおかしいので死亡モーション終了時にAnimation Eventで非アクティブにしています。
これで攻撃を受けた敵が倒れるようになりました!
~完~
最後まで読んで頂き、ありがとうございます!
Questのハンドトラッキングは楽しいので是非試してみてください。
好きなアニメの再現をするとワクワクするのでおすすめです!