Created
February 15, 2026 12:29
-
-
Save taroyanaka/f3d392ba732e5fb72e642e09aa424e1f to your computer and use it in GitHub Desktop.
unity6
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Reflection; | |
| using UnityEngine; | |
| /// <summary> | |
| /// Boss 用 Enemy 拡張。 | |
| /// - Bullet prefab を複数アタッチ可能 | |
| /// - Effect prefab を複数アタッチ可能 | |
| /// | |
| /// 既存ファイル(Enemy.cs 等)を変更せずに、 | |
| /// Inspector から差し替え・複数登録できるようにするためのコンポーネントです。 | |
| /// </summary> | |
| [DisallowMultipleComponent] | |
| public sealed class BossEnemy : Enemy | |
| { | |
| [Header("Prefabs (Multiple)")] | |
| [Tooltip("発射に使う弾プレハブ群(複数登録可)")] | |
| [SerializeField] private GameObject[] bulletPrefabs; | |
| [Tooltip("再生したいエフェクトプレハブ群(複数登録可)")] | |
| [SerializeField] private GameObject[] effectPrefabs; | |
| [Header("Spawn Points")] | |
| [Tooltip("弾の生成位置。未設定ならこの Transform")] | |
| [SerializeField] private Transform firePoint; | |
| [Tooltip("エフェクトの生成位置。未設定ならこの Transform")] | |
| [SerializeField] private Transform effectPoint; | |
| [Header("Aim")] | |
| [Tooltip("狙うターゲット。未設定なら自身の forward 方向に発射")] | |
| [SerializeField] private Transform aimTarget; | |
| [Tooltip("弾の Launch(owner, dir) がある場合に渡す owner。未設定ならこの Transform")] | |
| [SerializeField] private Transform projectileOwner; | |
| [Header("Projectile")] | |
| [Tooltip("生成した弾に ProjectileLayerUtility を適用")] | |
| [SerializeField] private bool configureProjectileLayer = true; | |
| [Tooltip("弾プレハブに Launch が無い場合のフォールバック速度(Rigidbody に設定)")] | |
| [Min(0.1f)] | |
| [SerializeField] private float fallbackProjectileSpeed = 30f; | |
| [Header("Selection")] | |
| [Tooltip("現在使用する bulletPrefabs のインデックス")] | |
| [SerializeField] private int currentBulletIndex; | |
| [Tooltip("現在使用する effectPrefabs のインデックス")] | |
| [SerializeField] private int currentEffectIndex; | |
| [Header("Debug")] | |
| [SerializeField] private bool debugLog; | |
| [Header("Drop")] | |
| [Tooltip("BossEnemy が倒されたときにドロップ(生成)する prefab 群(複数登録可)")] | |
| [SerializeField] private GameObject[] dropPrefabs; | |
| [Tooltip("倒されたときにドロップするか")] | |
| [SerializeField] private bool dropOnDeath = true; | |
| [Tooltip("true: ランダムに選んで指定数ドロップ / false: 登録したものを全てドロップ")] | |
| [SerializeField] private bool dropRandom = true; | |
| [Min(1)] | |
| [Tooltip("dropRandom=true のときに生成する個数")] | |
| [SerializeField] private int dropCount = 1; | |
| [Min(0f)] | |
| [Tooltip("ドロップ位置のばらつき(XZ 平面)")] | |
| [SerializeField] private float dropRadius = 0.6f; | |
| [Tooltip("ドロップ位置のオフセット")] | |
| [SerializeField] private Vector3 dropOffset = new Vector3(0f, 0.2f, 0f); | |
| private bool hasDropped; | |
| private void Awake() | |
| { | |
| if (firePoint == null) firePoint = transform; | |
| if (effectPoint == null) effectPoint = transform; | |
| if (projectileOwner == null) projectileOwner = transform; | |
| currentBulletIndex = NormalizeIndex(bulletPrefabs, currentBulletIndex); | |
| currentEffectIndex = NormalizeIndex(effectPrefabs, currentEffectIndex); | |
| } | |
| private void OnDestroy() | |
| { | |
| if (!Application.isPlaying) return; | |
| if (!dropOnDeath) return; | |
| if (hasDropped) return; | |
| // Enemy は hp<=0 で Destroy(gameObject) する。 | |
| // 他要因(シーンアンロード等)で Destroy された場合は hp が残っている想定なので除外。 | |
| if (hp > 0) return; | |
| hasDropped = true; | |
| DropOnDeath(); | |
| } | |
| private void DropOnDeath() | |
| { | |
| if (dropPrefabs == null || dropPrefabs.Length == 0) return; | |
| Vector3 basePos = transform.position + dropOffset; | |
| if (!dropRandom) | |
| { | |
| for (int i = 0; i < dropPrefabs.Length; i++) | |
| { | |
| var prefab = dropPrefabs[i]; | |
| if (prefab == null) continue; | |
| Instantiate(prefab, GetDropPosition(basePos), Quaternion.identity); | |
| } | |
| if (debugLog) | |
| { | |
| Debug.Log($"[BossEnemy] DropOnDeath all count={dropPrefabs.Length}", this); | |
| } | |
| return; | |
| } | |
| int spawnCount = Mathf.Max(1, dropCount); | |
| for (int i = 0; i < spawnCount; i++) | |
| { | |
| GameObject prefab = dropPrefabs[UnityEngine.Random.Range(0, dropPrefabs.Length)]; | |
| if (prefab == null) continue; | |
| Instantiate(prefab, GetDropPosition(basePos), Quaternion.identity); | |
| } | |
| if (debugLog) | |
| { | |
| Debug.Log($"[BossEnemy] DropOnDeath random count={spawnCount} pool={dropPrefabs.Length}", this); | |
| } | |
| } | |
| private Vector3 GetDropPosition(Vector3 basePos) | |
| { | |
| if (dropRadius <= 0f) return basePos; | |
| Vector2 r = UnityEngine.Random.insideUnitCircle * dropRadius; | |
| return basePos + new Vector3(r.x, 0f, r.y); | |
| } | |
| public void SetBulletIndex(int index) | |
| { | |
| currentBulletIndex = NormalizeIndex(bulletPrefabs, index); | |
| } | |
| public void SetEffectIndex(int index) | |
| { | |
| currentEffectIndex = NormalizeIndex(effectPrefabs, index); | |
| } | |
| /// <summary> | |
| /// Animation Event などから呼ぶ想定。 | |
| /// 現在の bullet を発射します。 | |
| /// </summary> | |
| public void FireCurrentBullet() | |
| { | |
| FireBullet(currentBulletIndex); | |
| } | |
| /// <summary> | |
| /// 指定インデックスの bullet を発射します。 | |
| /// </summary> | |
| public void FireBullet(int index) | |
| { | |
| GameObject prefab = GetByIndex(bulletPrefabs, index); | |
| if (prefab == null) return; | |
| Transform fp = firePoint != null ? firePoint : transform; | |
| Vector3 dir = GetAimDirection(fp); | |
| Quaternion rot = dir.sqrMagnitude > 0.0001f | |
| ? Quaternion.LookRotation(dir, Vector3.up) | |
| : fp.rotation; | |
| GameObject projectile = Instantiate(prefab, fp.position, rot); | |
| if (configureProjectileLayer) | |
| { | |
| ProjectileLayerUtility.ConfigureProjectileInstance(projectile, debugLog); | |
| } | |
| bool launched = TryInvokeLaunch(projectile, projectileOwner != null ? projectileOwner : transform, dir); | |
| if (!launched) | |
| { | |
| ApplyFallbackVelocity(projectile, dir); | |
| } | |
| if (debugLog) | |
| { | |
| Debug.Log($"[BossEnemy] FireBullet index={index} prefab='{prefab.name}' launched={launched}", this); | |
| } | |
| } | |
| public void FireRandomBullet() | |
| { | |
| if (bulletPrefabs == null || bulletPrefabs.Length == 0) return; | |
| FireBullet(UnityEngine.Random.Range(0, bulletPrefabs.Length)); | |
| } | |
| /// <summary> | |
| /// 現在の effect を再生(生成)します。 | |
| /// </summary> | |
| public void PlayCurrentEffect() | |
| { | |
| PlayEffect(currentEffectIndex); | |
| } | |
| /// <summary> | |
| /// 指定インデックスの effect を再生(生成)します。 | |
| /// </summary> | |
| public void PlayEffect(int index) | |
| { | |
| GameObject prefab = GetByIndex(effectPrefabs, index); | |
| if (prefab == null) return; | |
| Transform ep = effectPoint != null ? effectPoint : transform; | |
| Instantiate(prefab, ep.position, ep.rotation); | |
| if (debugLog) | |
| { | |
| Debug.Log($"[BossEnemy] PlayEffect index={index} prefab='{prefab.name}'", this); | |
| } | |
| } | |
| public void PlayRandomEffect() | |
| { | |
| if (effectPrefabs == null || effectPrefabs.Length == 0) return; | |
| PlayEffect(UnityEngine.Random.Range(0, effectPrefabs.Length)); | |
| } | |
| private Vector3 GetAimDirection(Transform from) | |
| { | |
| if (from == null) return Vector3.forward; | |
| if (aimTarget != null) | |
| { | |
| Vector3 d = (aimTarget.position - from.position); | |
| if (d.sqrMagnitude > 0.0001f) return d.normalized; | |
| } | |
| Vector3 fwd = from.forward; | |
| if (fwd.sqrMagnitude > 0.0001f) return fwd.normalized; | |
| return Vector3.forward; | |
| } | |
| private static int NormalizeIndex(GameObject[] array, int index) | |
| { | |
| if (array == null || array.Length <= 0) return 0; | |
| if (index < 0) return 0; | |
| if (index >= array.Length) return array.Length - 1; | |
| return index; | |
| } | |
| private static GameObject GetByIndex(GameObject[] array, int index) | |
| { | |
| if (array == null || array.Length <= 0) return null; | |
| if (index < 0 || index >= array.Length) return null; | |
| return array[index]; | |
| } | |
| private static bool TryInvokeLaunch(GameObject projectileInstance, Transform owner, Vector3 direction) | |
| { | |
| if (projectileInstance == null) return false; | |
| // 方向がゼロに近い場合、Launch を呼んでも意味が薄いので弾の forward を採用 | |
| if (direction.sqrMagnitude < 0.0001f) | |
| { | |
| direction = projectileInstance.transform.forward; | |
| } | |
| var behaviours = projectileInstance.GetComponents<MonoBehaviour>(); | |
| for (int i = 0; i < behaviours.Length; i++) | |
| { | |
| var mb = behaviours[i]; | |
| if (mb == null) continue; | |
| MethodInfo mi = mb.GetType().GetMethod( | |
| "Launch", | |
| BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, | |
| binder: null, | |
| types: new[] { typeof(Transform), typeof(Vector3) }, | |
| modifiers: null | |
| ); | |
| if (mi == null) continue; | |
| try | |
| { | |
| mi.Invoke(mb, new object[] { owner, direction }); | |
| return true; | |
| } | |
| catch (TargetInvocationException) | |
| { | |
| return false; | |
| } | |
| catch (ArgumentException) | |
| { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| private void ApplyFallbackVelocity(GameObject projectileInstance, Vector3 direction) | |
| { | |
| if (projectileInstance == null) return; | |
| if (direction.sqrMagnitude < 0.0001f) | |
| { | |
| direction = projectileInstance.transform.forward; | |
| } | |
| var rb = projectileInstance.GetComponent<Rigidbody>(); | |
| if (rb == null) | |
| { | |
| rb = projectileInstance.GetComponentInChildren<Rigidbody>(); | |
| } | |
| if (rb != null) | |
| { | |
| rb.linearVelocity = direction.normalized * fallbackProjectileSpeed; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment