Skip to content

Instantly share code, notes, and snippets.

@taroyanaka
Created February 15, 2026 12:29
Show Gist options
  • Select an option

  • Save taroyanaka/f3d392ba732e5fb72e642e09aa424e1f to your computer and use it in GitHub Desktop.

Select an option

Save taroyanaka/f3d392ba732e5fb72e642e09aa424e1f to your computer and use it in GitHub Desktop.
unity6
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