Unityゲーム開発で絶対役立つ小ネタスクリプト集2
スマホアプリ・マリンスクールシミュレーターの開発で、今後のゲーム開発に役立ちそうなノウハウはたくさん詰めました。
(採算はDL数からお察し)
3年の素人開発者経験から、unity c#でできることできないことを身をもって理解し、「かゆいところに手が届かない」を安直に解消する為のテクとスクリプトが溜まったので、一部紹介します。
UIHoldButton
長押し操作のUIのためのスクリプトです。
※inputsystemは非対応
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Events;
namespace FUNSET
{
// [RequireComponent(typeof( EventTrigger))]
/// <summary>
/// 長押しボタン。
/// </summary>
public class UIHoldButton : MonoBehaviour
{
public RectTransform Gauge;
[SerializeField, Header("長押しゲージ開始サイズ")
float Startval;
[SerializeField, Header("長押しゲージ終了サイズ")
float EndVal;
bool isPressDown = false;
[SerializeField, Header("長押し時間")]
float PressTime = 2f;
float PressCounter;
[SerializeField, Header("一度しか使わない")]
bool OnceUse=true;
bool CanUse=true;
public UnityEvent OnHoldEnd;
void Awake()
{
var ga = Gauge.localScale;
ga.x = Startval;
Gauge.localScale = ga;
valcount= Startval;
}
private void Update()
{
if (!CanUse) { return; }
var dt = Time.deltaTime;
if (isPressDown) { PressCounter += dt; }
//長押し時のゲージの挙動
valcount = EndVal;
var ga = Gauge.localScale;
ga.x = PressCounter / PressTime * (EndVal - Startval) + Startval;
Gauge.localScale = ga;
//満了判定
if ((PressCounter >= PressTime)&&(isPressDown))
{
PressCounter = 0;
isPressDown = false;
OnHoldEnd.Invoke();
if (OnceUse) { CanUse = false; }
}
}
//EventTriggerのPointerDownイベントに登録する処理
public void PointerDown()
{
if (!CanUse) { return; }
isPressDown = true;
}
//EventTriggerのPointerUpイベントに登録する処理
public void PointerUp()
{
if (!CanUse) { return; }
isPressDown = false;
}
}
}
このスクリプトとイベントトリガーと、ボタン用画像とゲージ用画像を組み合わせて作ります。適当なボタンとスプライト画像のアセットを調達してお試しください。
イベントトリガーには、PointerDown()、PointerUp()をアタッチして、割り当てます。(実行時に自動でイベントトリガーを足してAddListnerで足してもいいけど、わかりやすさ優先)
このスクリプトでは、単純に長押しインジケータをゲージ用画像のxスケールを変えて表現しています。凝った長押しUIにしたい場合は改造しましょう
ここまで用意できたらStartvalにゲージ空状態のxスケール数値、EndValにゲージ満タンのxスケール数値をセット、
イベントトリガーをセット、
OnHoldEndにゲージ満タンになったときに実行したいイベントをセット
これで、長押しするとゲージが伸び、放すと戻り、
満了するとOnHoldEndを実行します。
長押し操作の利点は、キャラ操作の幅が増えるのはもちろん、重要な決定の操作にYes/NoのダイアログUIとスクリプトを作らなくて済むという点です。
TriggerSender
これをコライダーとRigidbodyを持つオブジェクトにセットしますと、通常、そのオブジェクトにしか渡されないコリジョンやトリガーにかかったother.gameobjectの情報を、他のオブジェクトに渡せるようにできます。コライダーがトリガーであってもなくても同様に保持できます。
また、衝突した瞬間だけ保持させることもできます。
タグ・レイヤーの選別だけでなく、除外用のタグ・レイヤーも対応してます。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Events;
namespace FUNSET
{
public class TriggerSender : MonoBehaviour
{
public GameObject otherObject;
public enum ColliderType{ Collider=0, Collider2D=1, Trigger=2, Trigger2D=3 }
[SerializeField, Header("使用するコライダーのタイプ")]
public ColliderType colliderType;
Collider2D collider2D;
Collider collider;
Collision2D collision2D;
Collision collision;
[SerializeField, Header("タグで判定")]
public bool UseTag;
[SerializeField, Header("判定するタグ")]
public string TargetTag = "Player";
[SerializeField, Header("レイヤーマスクで判定")]
public bool UseLayer;
[SerializeField, Header("判定するレイヤーマスク")]
public LayerMask layerDetect = 0 << 1;
[SerializeField, Header("タグで除外判定")]
bool UseexclusionTag;
[SerializeField, Header("判定する除外タグ")]
public string exclusionTag = "Enemy";
[SerializeField, Header("レイヤーマスクで除外判定")]
bool UseexclusionLayer;
[SerializeField, Header("判定する除外レイヤーマスク")]
public LayerMask layerexclusion = 0 << 1;
[SerializeField, Header("otherObjectを衝突した瞬間だけ保持")]
public bool OnlyFrame;
GameObject Preother;
public void Awake()
{
switch (colliderType)
{
case ColliderType.Collider:
collider = GetComponent<Collider>();
break;
case ColliderType.Collider2D:
collider2D = GetComponent<Collider2D>();
break;
case ColliderType.Trigger:
collision = GetComponent<Collision>();
break;
case ColliderType.Trigger2D:
collision2D = GetComponent<Collision2D>();
break;
default:
break;
}
}
public void Start()
{
}
/// <summary>
///
/// </summary>
/// <param name="obj"></param>
/// <param name="mask"></param>
/// <returns></returns>
bool IsInLayerMask(GameObject obj, LayerMask mask)
{
return ((mask.value & (1 << obj.layer)) > 0);
}
private void OnTriggerEnter2D(Collider2D other)
{
if ((UseTag) && (other.gameObject.tag != TargetTag)) { return; }
if ((UseLayer) && (!IsInLayerMask(other.gameObject, layerDetect))) { return; }
if ((UseexclusionTag) && (other.gameObject.tag == exclusionTag)) { return; }
if ((UseexclusionLayer) && (IsInLayerMask(other.gameObject, layerexclusion))) { return; }
otherObject = other.gameObject;
}
private void OnTriggerExit2D(Collider2D other)
{
otherObject = other.gameObject;
}
private void OnTriggerEnter(Collider other)
{
if ((UseTag) && (other.gameObject.tag != TargetTag)) { return; }
if ((UseLayer) && (!IsInLayerMask(other.gameObject, layerDetect))) { return; }
if ((UseexclusionTag) && (other.gameObject.tag == exclusionTag)) { return; }
if ((UseexclusionLayer) && (IsInLayerMask(other.gameObject, layerexclusion))) { return; }
otherObject = other.gameObject;
}
private void OnTriggerExit(Collider other)
{
otherObject = null;
}
private void OnCollisionEnter2D(Collision2D collision)
{
if ((UseTag) && (collision.gameObject.tag != TargetTag)) { return; }
if ((UseLayer) && (!IsInLayerMask(collision.gameObject, layerDetect))) { return; }
if ((UseexclusionTag) && (collision.gameObject.tag == exclusionTag)) { return; }
if ((UseexclusionLayer) && (IsInLayerMask(collision.gameObject, layerexclusion))) { return; }
otherObject = collision.gameObject;
}
private void OnCollisionExit2D(Collision2D collision)
{
otherObject = null;
}
private void OnCollisionEnter(Collision collision)
{
if ((UseTag) && (collision.gameObject.tag != TargetTag)) { return; }
if ((UseLayer) && (!IsInLayerMask(collision.gameObject, layerDetect))) { return; }
if ((UseexclusionTag) && (collision.gameObject.tag == exclusionTag)) { return; }
if ((UseexclusionLayer) && (IsInLayerMask(collision.gameObject, layerexclusion))) { return; }
otherObject = collision.gameObject;
}
private void OnCollisionExit(Collision collision)
{
otherObject = null;
}
private void Update()
{
if (OnlyFrame)
{
if (Preother != null)
{
if (Preother == otherObject)
{
otherObject = null;
}
}
}
Preother = otherObject;
}
}
}
3D・2D・トリガー・コライダー全対応にしたせいで冗長です
ぶつかった情報はotherObjectに保持します。otherObjectを取得・監視する別のコンポーネントとペアで使います。(それは自作してください)
スキンメッシュキャラで、全身のボーンに当たり判定判定をつけて一元管理する事ができます。
Rootにコライダーを置いて判定するやりかただと、大型のキャラを正確な当たり判定にできない問題が出ます。ヒットコライダーはスキニングモデルのボーンの階層にアタッチすることで、正確に当たり判定ができます。
他には、セーブポイントや触れたら即死なトラップなど、シーン上に複数あるコライダーで同じ処理をさせたいとかでも役立つでしょう。
その場合、コライダー管理元を実行時にシーン上から自動取得するようにカスタムすると、ゲーム開発がサクサクになります。
選別除外用のタグ・レイヤーの使い道は、同じスクリプトで衝突判定する弾のプレハブを敵もプレイヤーも発射するが、撃った者の体と味方には当たらなくさせたい(しかも発射口が体の当たり判定より内にある)、などで役立ちます。タグ>レイヤーマスク>除外タグ>除外レイヤーマスクの順で選別します
DestroyDelay
遅延でオブジェクトを削除します。パーティクルのアセットに大抵ついてるけど、油断してるとプロジェクトになくて困るやつです。
using UnityEngine;
namespace FUNSET
{
public class DestroyDelay : MonoBehaviour
{
[SerializeField]
float DestroyTime;
float Dcount;
void Update()
{
Dcount += Time.deltaTime;
if (Dcount > DestroyTime)
{
Destroy(this.gameObject);
}
}
}
}
Unityの通常のDestroyは、シーンロード時にひどい不具合を出すのでうかつに使えません。ディレイでワンクッションおいて使えば安全です。
ActiveSyncer
一つのオブジェクトの表示非表示を常時監視して、他のオブジェクトも同期させられます。
安直ですが超使えます。
using UnityEngine;
namespace FUNSET
{
public class ActiveSyncer : MonoBehaviour
{
[SerializeField, Header("同期元オブジェクト")]
GameObject ReferObject;
[SerializeField, Header("同期させたいオブジェクト")]
GameObject[] SyncObjects;
[SerializeField, Header("アクティブ反転"), Tooltip("同期元オブジェクトがアクティブのとき同期させたいオブジェクトは非アクティブ")]
bool Reverse;
public void Update()
{
if (ReferObject == null) return;
for (int i = 0; i < SyncObjects.Length; i++)
{
if (SyncObjects[i] != null)
{
if (!Reverse) { SyncObjects[i].SetActive(ReferObject.activeInHierarchy); }
else
{
SyncObjects[i].SetActive(!ReferObject.activeInHierarchy);
}
}
}
}
}
}
タブ的なUI(パッド操作専用などタッチ・マウスが使えない状況でタブ切り替えさせたいとき)、スイッチで動作するステージギミックを作るのに
どれもチープすぎて有料アセットで販売するもんじゃないけど、
Unityに基礎機能でついてくれよぐらいの実用性があるでしょう。
3年で得たゲームプログラミングの真理は、「基礎で単純なコードは早くて、安定する」でした。
コルーチンやインターフェース使わないの?て思いそうですが現実には、なぜか情報を渡さない・仕事しない時があって、そうなった時は別系統で同様の機能を持つスクリプトを調達せざるを得なくて…身の丈に合わない高度な機能は使ってもろくなことにならん。
なお、ここに記したスクリプトは実際のゲームに使ったものから未使用の変数とか試行錯誤の跡とかを削って若干改変してテストしてないので、完全な動作保証はしませんのでご了承ください。