
Unityゲーム役立ち小ネタスクリプト5:安定したインタラクトボタン
マリンスクールシミュレーターPC版を作り進めています。
すごい遅れてます。モチベが上がらない…
操作系統とUIをPC用に作り直すだけでも、違法建築ジェンガタワーのごときプロジェクトを倒壊しないようにやるとなると酷い苦行です。
見栄えが一切変わらないとこの修正って苦労が伝えられなくてつらいです。

それはおいといて、
NPCに近寄ってAボタンで会話する、落ちてるアイテムやインタラクトに近づいてAボタン長押しで拾う/オブジェクトを操作する、などを実現するにはコライダー・トリガーとOnTriggerEnter()を用いますが、
3Dゲームでやるとなると、それだけでは不十分なことがわかりました。
※ここからの話は、初歩のUnityゲームプログラムスキルを習得済みで、2D・3Dアクションゲームのキャラクターコントローラをなんとか扱えるぐらいの腕前の方をを想定しています。

1.プレイヤーキャラにインタラクト用トリガーをつける
2.(歩いて近寄って)何かしらのコライダーに触れたら、そのオブジェクトが目当てのターゲットなのかを、タグやレイヤーやgetcomponentなりで探る
3.条件が合致したら操作用のUIをアクティブにして、ボタン操作したらインタラクトを実行…とやるのですが、
問題はターゲットがトリガーから外れた場合で…
普通それはOnTriggerExit()で検知して操作用のUIを非アクティブにするのですが、実際には、プレイヤーが1フレームでもよそ見したり、ターゲットのオブジェクトが小さかったりうろちょろするNPCだったら、ほんとに一瞬で外されて非アクティブになってしまいます。
コライダーを大きくしたりOnTriggerStayを使えば解決する場合もありますが、それでも追いつかないほど早く消える場合がありました。
どうも、ずっと接触してるはずのコライダーが一瞬だけ検知をロストしてOnTriggerExit()扱いになる場合があるようでした。
Unityの仕様上ではそんなことおこるはずないと思うのですが、他アセットのせいで起こることもあるのでしょう。3Dゲームでは高度なことやってるアセットを使うと思わぬ不具合をもたらし、高度ゆえに直せないこともあります。独自のオクルージョンカリングでオブジェクトを非アクティブにするアセットとか使うので、そのあたりが怪しいかなあ…とか。
そこで、一度補足したターゲットをなにがなんでもしばらく保持して離さない仕組みを作りました。
サンプルコード
例えば、プレイヤーと会話できるNPCを用意して、近づいたら会話ボタンが出現して会話可能になる、という仕様を実装したいとします、
まず、会話するにはターゲットオブジェクトに会話システムのアセット・KaiwaComponentをアタッチしてある、とします。(会話アセットは自前で用意してください。)
それから、プレイヤーの情報を保持するクラスかデータベースdataPlayerStatus(scriptableObject)とその中に、IsKaiwa(bool)とCanKaiwa(bool)を用意します。
IsKaiwa すでに会話中かどうか
CanKaiwa 会話ボタンがすでにアクティブかどうか
この前提で、こうコードを書きます。
※このコードは参考用です。そのままコピペしても使えません
[SerializeField, Header("会話判定用タグ")]
string TagofDSTALK = "NPC";
[SerializeField, Header("会話ロックオン距離")]
float LockOnDistance=2.0f;
[SerializeField, Header("会話ロックオンタイマー")]
float LockOnTimer = 2.0f;
float LockOnCount;
GameObject LockOnTarget;
public void OnTriggerEnter(Collider other)
{
//会話中なら無効
if (dataPlayerStatus.IsKaiwa) { return; }
if (other.tag == TagofTALK)
{
//会話を制御するコンポーネント・KaiwaComponentの取得を試みる
var a = other.gameObject.GetComponent<KaiwaComponent>();
//コンポーネントを取得できて会話ボタンが表示されてなければ、会話可能ターゲットを保持する
if ((a != null) && (dataPlayerStatus.CanKaiwa == false))
{
dataPlayerStatus.CanKaiwa = true;
LockOnTarget = a.gameObject;
}
}
}
public void Update()
{
CheckTargetKeep();
}
public void CheckTargetKeep()
{
if(dataPlayerStatus.CanKaiwa == true)
{
//会話ボタンを表示して、会話ボタンの入力受付する処理をアクティブにする処理を書きます
}
else
{
//会話ボタンを非表示にして、会話ボタンの入力受付する処理を非アクティブにする処理を書きます
}
//会話可能ターゲットのロックオンを判定します
if (LockOnTarget != null)
{
//ターゲットとの距離を判定します
var dis = (this.transform.position - LockOnTarget.transform.position).sqrMagnitude;
var dt = Time.deltaTime;
if (dis * dis > LockOnDistance*LockOnDistance) { LockOnCount += dt; } else { LockOnCount = 0; }
//タイマーが満了するか極端に距離が離れたらターゲットを解除します
if ((LockOnCount > LockOnTimer)||(dis * dis>50f)) { LockOffKaiwa(); }
}
}
// 会話可能ターゲットのロックオンを解除します
public void LockOffKaiwa()
{
if (LockOnTarget == null) return;
LockOnTarget = null;
LockOnCount = 0;
dataPlayerStatus.CanKaiwa = false;
}
// 会話します
public void ExeKaiwa()
{
dataPlayerStatus.CanKaiwa = false;
//会話アセット・KaiwaComponentの実行処理をここに書く
}
これに加えて、CanKaiwaとIsKaiwaを毎フレーム監視し、CanKaiwaがfalseの間は会話可能アイコンと入力受付が非アクティブになるというコードを別の場所に書きます。
//会話可能状態なことをプレイヤーに見せるアイコン
public GameObject UITalk;
public void Update()
{
if(dataPlayerStatus.CanKaiwa)
{
UITalk.SetActive(true);
//入力受付をアクティブにするコードを書きます
}
else
{
UITalk.SetActive(false);
//入力受付を非アクティブにするコードを書きます
}
}
こんな感じのを空オブジェクトにアタッチしてシーンのどこかに置いてボタン操作とUIを制御させます。会話ボタンを押したらExeKaiwa()を呼び出すように設定すれば、無事に会話ができます。

プレイヤーがターゲットオブジェクトと接触すると、LockOnTarget にターゲットの情報を代入。
距離が離れるとカウントが始まり、一定時間経過か距離が離れたら保持は解除されるでしょう。(状況によってはIsKaiwaがfalseの判定もいるかも)
ターゲットを保持してる間は、会話ボタンを押せば会話がはじまり、会話可能アイコンと入力受付は非アクティブになります。
会話せず一定時間経過するか距離が離れるとターゲットが解除され、会話可能アイコンと入力受付は非アクティブになります。
会話中断/終了のさいにLockOffKaiwa()を呼び出すように設定すれば、ターゲットをクリアします。
保持してる僅かな時間内でもターゲットが非アクティブや消えるとかがあるゲームシステムな場合は、LockOnTargetのnull判定をもっと入念にする必要があります。
また実際のゲームでは、同じトリガーでインタラクトの操作やアイテムの自動拾いなども行うことになります。
それらが密集した場所で同時に実行できないようにうまく制御するのはまあ、テストプレイ的な意味で大変です。
会話する直前に敵に襲われたとか、物理の効いた拾えるアイテムを蹴飛ばしたりなどの、何かの要因で会話ボタン・拾えるボタンが残ったままになるとかになります。どう対策しても…ほんとにこれがもう…
私のモデリングとゲーム開発の最新情報は、Discoadを御覧ください。
https://discord.gg/Ur2pmF7ptw