<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AKUtil
{
    /// <summary>
    /// singletonパターンで実装されるmonobehaviour
    /// 参照時にインスタンスが存在しない場合、gameobjectにadd componentされて自動生成される。
    /// デフォルトで、DontDestroyOnLoad しません。
    /// </summary>

    public class SingletonMonoBehaviour<T> : MonoBehaviour where T : SingletonMonoBehaviour<T>
    {
        protected static T instance;
        public static T Instance {get { return CreateInstance(); } }

        public static T CreateInstance()
        {
            if (instance != null) return instance;

            instance = (T)FindObjectOfType(typeof(T));

            if (instance != null) return instance;

            var name = typeof(T).Name;
            Debug.LogFormat("Create singleton object: {0}", name);
            GameObject obj = new GameObject(name);
            instance = obj.AddComponent<T>();

            if (instance != null) return instance;

            Debug.LogWarning("Can't find singleton object: " + typeof(T).Name);
            Debug.LogError("Can't create singleton object: " + typeof(T).Name);

            return null;
        }
        public static bool IsInstantiated()
        {
            return (instance != (MonoBehaviour)null);
        }

        private void Awake()
        {
            if (CheckInstance())AwakeValidly();
        }

        protected virtual void AwakeValidly()
        {
            // do nothing
        }

        private bool CheckInstance()
        {
            if (instance == null)
            {
                instance = (T)this;
                return true;
            }

            if (Instance == this) return true;

            Destroy(this);
            return false;
        }

        protected void DontDestroyOnLoad()
        {
            GameObject.DontDestroyOnLoad(this.gameObject);
        }
    }
}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

/// <summary>
/// 移動方向に関する基底クラス
/// </summary>
public class IFTOInput : MonoBehaviour
{
    protected Subject<IInteractive> interactionSubject = new Subject<IInteractive>();
    public IObservable<IInteractive> OnInteract { get { return interactionSubject; } }

    Vector3 direction;

    bool isLocked = false;
    public bool IsLocked { get { return isLocked; } set { if(value)Direction = Vector3.zero; isLocked = value; } }

    public void Reset()
    {
        direction = Vector3.zero;
    }

    public Vector3 Direction
    {
        get { return direction; }
        protected set
        {
            direction = value;
            if (direction.magnitude > 1f) direction = direction.normalized;
        }
    }
}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

public interface IInteractive
{
    void Interact(InteractionType interactionType);
    InteractionType GetInteractionType();
    Vector3 GetPosition();
    OutfitData GetDisguiseOutfit();
}

public enum InteractionType
{
    Kill,
    Pickup,
    Use,
    Disguise,
    Locked,
}<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "OutfitData", menuName = "ScriptableObjects/OutfitData")]
public class OutfitData : ScriptableObject
{
    public string Name;
    public Outfit Outfit;
    public OutfitType Type;
}<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

/// <summary>
/// Outfit、外見を変更するクラス
/// </summary>
public class OutfitHundler : MonoBehaviour
{
    [SerializeField] Transform outfitParent;

    private ReactiveProperty<OutfitData> rpCurrentOutfit = new ReactiveProperty<OutfitData>();
    public OutfitData CurrentOutfit { private set { rpCurrentOutfit.Value = value; } get { return rpCurrentOutfit.Value; } }
    public IObservable<OutfitData> OnOutfitChanged { get { return rpCurrentOutfit; } }

    Outfit outfit;
    RelationshipHundler relationshipHundler;
    bool isPlayer = false;

    public Animator Animator { get { return outfit.Animator; } }

    public bool IsNaked
    {
        get { return CurrentOutfit.Type == OutfitType.Naked; }
    }

    public void Initialize(OutfitData outfit, RelationshipHundler relationshipHundler)
    {
        SetOutfit(outfit);
        this.relationshipHundler = relationshipHundler;
        ChangeColorImmediately();
        relationshipHundler.OnRelationshipChanged
            .Subscribe(_ => ChangeColorImmediately());
    }

    void ChangeColorImmediately()
    {
        var color = GetColor(relationshipHundler.CurrentRelationshop);
        SetOutline(color);
    }

    void SetOutline(Color color)
    {
        foreach (var renderer in outfit.Renderers)
        {
            foreach (var mat in renderer.materials)
            {
                mat.SetColor("_OutlineColor", color);
            }
        }
    }

    void SetOutlineWhite()
    {
        SetOutline(Color.white);
    }

    Color GetColor(RelationshipType type)
    {
        switch (type)
        {
            case RelationshipType.Hostile:
                return Color.red;
            case RelationshipType.Friendly:
                return Color.green;
            default:
                return Color.green;
        }
    }

    public void SetOutfit(OutfitData next, bool isPlayer = false)
    {
        if(isPlayer)this.isPlayer = true;
        CurrentOutfit = next;

        if(outfitParent.childCount >= 1)
        {
            DestroyImmediate(outfitParent.GetChild(0).gameObject);
        }

        outfit = Instantiate(CurrentOutfit.Outfit, outfitParent);
        outfit.transform.localPosition = Vector3.zero;
        outfit.transform.localRotation = Quaternion.identity;
        if (this.isPlayer) SetOutlineWhite();
    }

    public void BeNaked()
    {
        SetOutfit(DatasetLocator.Instance.NakedOutfitData);
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class GameClearView : MonoBehaviour
{
    [SerializeField] CanvasGroup canvasGroup;
    [SerializeField] Button continueButton;
    [SerializeField] Button exitButton;

    private void Start()
    {
        continueButton.onClick.AddListener(() => SceneManager.LoadScene("Main"));
        exitButton.onClick.AddListener(() => Application.Quit());
    }

    public void Show()
    {
        canvasGroup.alpha = 1;
        canvasGroup.interactable = true;
        canvasGroup.blocksRaycasts = true;
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(HumanController))]
public class TargetEnemy : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        GetComponent<HumanController>().OnDead += () => GameRuleManager.Instance.GameClear(); 
    }

}
<file_sep>﻿using System;
using UnityEngine;

public class AIPath : MonoBehaviour
{
    [SerializeField] private Point[] path;
    public Point[] Path { get { return path; } }

    [Serializable]
    public class Point
    {
        [SerializeField] Transform point;
        public Vector3 Position { get { return point.position; } }
        public Vector3 Forward { get { return point.forward; } }
        [Header("pointに到達後の待機時間")]
        public float WaitTime;
    }
}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class OutfitLimitedInteraction : MonoBehaviour, IInteractive
{
    [SerializeField] OutfitType targetOutfit;

    public Action OnInteracted;
    public InteractionType GetInteractionType()
    {
        var playerOutfit = GameRuleManager.Instance.Player.CurrentOutfitType;
        if (playerOutfit != targetOutfit) return InteractionType.Locked;
        return InteractionType.Use;
    }

    public Vector3 GetPosition()
    {
        return transform.position;
    }

    public OutfitData GetDisguiseOutfit()
    {
        return null;
    }

    public void Interact(InteractionType interactionType)
    {
        OnInteracted?.Invoke();
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Outfit : MonoBehaviour
{
    [SerializeField] Renderer[] renderers;
    [SerializeField] Animator animator;

    public Renderer[] Renderers { get { return renderers; } }
    public Animator Animator { get { return animator; } }
}

public enum OutfitType
{
    Police,
    Bandit,
    Chef,
    Naked,
    Vip,
    Guard,
    Worker
}<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class InteractButton : MonoBehaviour
{
    [SerializeField] Button button;
    [SerializeField] TextMeshProUGUI buttonText;
    [SerializeField] Player player;

    public Action<IInteractive> OnClick;

    public IInteractive TargetInteraction { get { return player.TargetInteraction; } }

    private void Start()
    {
        player.OnInteractionExit += SetDisable;
        player.OnInteractionUpdate += UpdateButtonView;

        UpdateButtonView(InteractionType.Kill);
        button.interactable = false;
        button.onClick.AddListener(() => OnClick?.Invoke(player.TargetInteraction));
    }

    void SetDisable()
    {
        button.interactable = false;
    }

    void UpdateButtonView(InteractionType type)
    {
        switch (type)
        {
            case InteractionType.Disguise:
                buttonText.text = "変装";
                button.interactable = true;
                break;
            case InteractionType.Kill:
                buttonText.text = "倒す";
                button.interactable = true;
                break;
            case InteractionType.Use:
                buttonText.text = "毒を盛る";
                button.interactable = true;
                break;
            case InteractionType.Locked:
                button.interactable = false;
                return;
            default:
                break;
        }
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "VisionMaterialData",menuName = "ScriptableObjects/CreateVisionMaterialData")]
public class VisionMaterialData : ScriptableObject
{
    public Material dangerMaterial;
    public Material safeMaterial;
    public Material detectedMaterial;
}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UniRx;
using AKUtil;

public class GameRuleManager : SingletonMonoBehaviour<GameRuleManager>
{
    [SerializeField] Player player;
    [SerializeField] GameClearView gameClearView;
    [SerializeField] GameClearView gameoverView;

    internal Player Player { get { return player; } }

    public static Action<Player> OnPlayerAppearanceChanged;

    bool isClear = false;

    public void GameClear()
    {
        isClear = true;
        StartCoroutine(GameClearCoroutine());
    }

    IEnumerator GameClearCoroutine()
    {
        yield return new WaitForSeconds(1);
        gameClearView.Show();

    }

    public void PlayPoisonFinisher()
    {
        isClear = true;
        StartCoroutine(PlayFinisherCoroutine());
    }

    IEnumerator PlayFinisherCoroutine()
    {
        yield return new WaitForSeconds(2f);
        SceneManager.LoadScene("Finisher");
    }

    public void Gameover()
    {
        if (isClear) return;
        StartCoroutine(GameoverCoroutine());
    }

    IEnumerator GameoverCoroutine()
    {
        yield return new WaitForSeconds(0.5f);
        gameoverView.Show();
    }



}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FinisherView : MonoBehaviour
{
    [SerializeField] GameClearView clearView;
    // Start is called before the first frame update
    IEnumerator Start()
    {
        yield return new WaitForSeconds(10);
        clearView.Show();

    }

}
<file_sep>﻿using System;
using UniRx;
using UnityEngine;

public class Enemy : MonoBehaviour, IInteractive
{
    [SerializeField] AIController ai;
    [SerializeField] CharacterDetector characterDetector;
    [SerializeField] OutfitHundler outfitHundler;
    [SerializeField] HumanController humanController;
     
    public InteractionType CurrentInteraction { private set; get; }

    void Start()
    {
        characterDetector.OnLost.Subscribe(_ => UpdateInteractionType());
        characterDetector.OnDetect.Subscribe(_ => UpdateInteractionType());
        ai.OnStateChanged.Subscribe(_ => UpdateInteractionType());
        outfitHundler.OnOutfitChanged.Subscribe(_ => UpdateInteractionType());
    }

    void UpdateInteractionType()
    {
        //敵が死んでいる かつ 洋服をきてる ならば変装可能。
        bool canDisguise = ai.IsDead() && !outfitHundler.IsNaked;
        if (canDisguise)
        {
            CurrentInteraction = InteractionType.Disguise;
            return;
        }

        //敵が死んでいて裸なら何もしない
        if (ai.IsDead())
        {
            CurrentInteraction = InteractionType.Locked;
            return;
        }

        //生きていて視界に入ってない ならばキルできる
        if (!characterDetector.IsInvision)
        {
            CurrentInteraction = InteractionType.Kill;
            return;
        }

        //生きていて視界に入っている ならば何もしない
        CurrentInteraction = InteractionType.Locked;
        return;
    }

    public void Interact(InteractionType interactionType)
    {
        humanController.Interacted(interactionType);
    }
    
    public InteractionType GetInteractionType()
    {
        return CurrentInteraction;
    }

    public Vector3 GetPosition()
    {
        return transform.position;
    }

    public OutfitData GetDisguiseOutfit()
    {
        return outfitHundler.CurrentOutfit;
    }
}<file_sep>﻿using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Assertions;
using UniRx;
using AKUtil;

/// <summary>
/// ステートベースの敵AI
/// パトロールして、敵を見つけたら追いかけて、見失ったらまた持ち場に戻る。
/// Navmeshによる経路探索を行い、その経路を利用して移動方向のinputを行う。
/// </summary>
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(OutfitHundler))]
[RequireComponent(typeof(CharacterDetector))]
[RequireComponent(typeof(HumanController))]
public class AIController : IFTOInput
{
    [SerializeField] AIPath aiPath;
    [SerializeField] EnemyData enemyData;

    OutfitHundler outfitHundler;
    CharacterDetector characterDetector;
    HumanController humanController;
    NavMeshAgent navMeshAgent;

    private StateMachine stateMachine = new StateMachine();
    private RelationshipHundler relationshipHundler;
    private IInteractive chaseTarget;

    private static readonly float arriveDiffThreshold = 0.1f;
    private static readonly float killDiffThreshold = 1.05f;

    public State Patrol = new State("Patrol");
    public State Chase = new State("Chase");
    public State Dead = new State("Dead");

    public bool IsDead()
    {
        return IsCurrentState(Dead);
    }

    //購読だけ公開する
    private Subject<RelationshipType> relationshipChangeSubject = new Subject<RelationshipType>();
    public IObservable<RelationshipType> OnRelationshipChanged { get { return relationshipChangeSubject; } }

    public IObservable<State> OnStateChanged { get { return stateMachine.OnStateChanged; } }

    private bool isHostile = false;

    public bool IsCurrentState(State target)
    {
        return target == stateMachine.CurrentState;
    }

    private void Awake()
    {
        Setup();
    }

    void Setup()
    {
        navMeshAgent = GetComponent<NavMeshAgent>();
        humanController = GetComponent<HumanController>();
        outfitHundler = GetComponent<OutfitHundler>();
        characterDetector = GetComponent<CharacterDetector>();

        bool isConflict = false;

        //遷移の設定と被りのチェックを行う
        isConflict |= Patrol > Chase;
        isConflict |= Chase > Patrol;
        isConflict |= Patrol > Dead;
        isConflict |= Chase > Dead;

        Assert.IsFalse(isConflict, "遷移が重複しています");

        relationshipHundler = new RelationshipHundler(enemyData);
        outfitHundler.Initialize(enemyData.OutfitData, relationshipHundler);

    }
    [ContextMenu("Setoutfit")]
    void Setoutfit()
    {
        var oh = GetComponent<OutfitHundler>();
        oh.SetOutfit(enemyData.OutfitData);
    }

    private void Start()
    {
        Patrol.OnStart.Subscribe(_ => OnStartPatrol());
        Patrol.OnUpdate.Subscribe(_ => UpdatePatrol());
        Chase.OnStart.Subscribe(_ => OnStartChase());
        Chase.OnUpdate.Subscribe(_ => OnUpdateChase());
        Chase.OnEnd.Subscribe(_ => OnEndChase());
        Dead.OnStart.Subscribe(_ => OnStartDead());

        stateMachine.SetInitialState(Patrol);

        characterDetector.SetRelationship(relationshipHundler);
        characterDetector.OnDetect.Subscribe(DetectCallback);
        characterDetector.OnLost.Subscribe(_ => LostCollback());

        humanController.OnDead += () => stateMachine.PushState(Dead);
    }

    int pathIndex = 0;
    int cornerIndex = 0;
    AIPath.Point currentTargetPoint;
    Vector3 currentCorner;
    Vector3 moveStartPoint;
    Vector3 positionBuffer;
    Vector3 lookDirection;
    float interval = 0;
    float timer = 0;

    //巡回するために最初の経路を設定する
    void OnStartPatrol()
    {
        Debug.Log("pat");
        moveStartPoint = transform.position;

        //pathが存在しない
        if (aiPath == null || aiPath.Path == null) return;
        currentTargetPoint = aiPath.Path[pathIndex];
        lookDirection = currentTargetPoint.Forward;
        navMeshAgent.SetDestination(currentTargetPoint.Position);
        moveStartPoint = transform.position;
        timer = 10000;
        Direction = Vector3.zero;
    }

    //設定されたパスに沿って巡回する
    void UpdatePatrol()
    {
        navMeshAgent.SetDestination(currentTargetPoint.Position);
        //その場で待機する
        if (timer < interval)
        {
            Direction = lookDirection * 0.001f;
            timer += Time.deltaTime;
            return;
        }

        //pathが存在しない
        if (aiPath == null || aiPath.Path == null) return;

        var navPath = navMeshAgent.path.corners;
        if (navPath.Length <= 0) return;

        //経路が存在しないか、十分近づいたら到着とみなす
        bool hasArrived = navPath.Length <= 1 || (transform.position - currentTargetPoint.Position).sqrMagnitude <= arriveDiffThreshold;
        if (hasArrived)
        {
            Direction = Vector3.zero;
            interval = aiPath.Path[pathIndex].WaitTime;
            lookDirection = currentTargetPoint.Forward;
            pathIndex++;
            pathIndex %= aiPath.Path.Length;

            moveStartPoint = transform.position;
            currentTargetPoint = aiPath.Path[pathIndex];
            navMeshAgent.SetDestination(currentTargetPoint.Position);
            //待機
            timer = 0;

            return;
        }

        //移動の入力を行う
        positionBuffer = transform.position;
        var posDiff = (navPath[1] - positionBuffer).normalized / 2;
        Direction = posDiff;

    }

    void LostCollback()
    {
        if (IsCurrentState(Patrol)) return;
        stateMachine.PushState(Patrol);
    }

    void DetectCallback(IInteractive target)
    {
        if (IsCurrentState(Chase)) return;
        chaseTarget = target;
        stateMachine.PushState(Chase);
    }

    void OnStartChase()
    {
        navMeshAgent.SetDestination(chaseTarget.GetPosition());
    }

    void OnUpdateChase()
    {
        navMeshAgent.SetDestination(chaseTarget.GetPosition());

        var navPath = navMeshAgent.path.corners;
        if (navPath.Length <= 0) return;
        //経路が存在しないか、十分近づいたら到着とみなす
        bool hasArrived = navPath.Length <= 1 || (transform.position - chaseTarget.GetPosition()).sqrMagnitude <= killDiffThreshold;
        if (hasArrived)
        {
            interactionSubject.OnNext(chaseTarget);
            stateMachine.PushState(Patrol);
            Direction = Vector3.zero;
            Debug.Log("Player killed");

            return;
        }

        //移動の入力を行う
        positionBuffer = transform.position;
        var posDiff = (navPath[1] - positionBuffer).normalized;
        Direction = posDiff;

    }

    void OnEndChase()
    {
        Direction = Vector3.zero;
    }
    void OnStartDead()
    {
        characterDetector.Sleep();
        Direction = Vector3.zero;
    }

    private void Update()
    {
        stateMachine.Update();
    }

    private void FixedUpdate()
    {
        stateMachine.FixedUpdate();
    }

    private void LateUpdate()
    {
        stateMachine.LateUpdate();
    }

    /// <summary>
    /// ギズモに経路を表示
    /// </summary>
    void OnDrawGizmos()
    {
        if (navMeshAgent && navMeshAgent.enabled)
        {
            Gizmos.color = Color.red;
            var prefPos = transform.position;

            foreach (var pos in navMeshAgent.path.corners)
            {
                Gizmos.DrawLine(prefPos, pos);
                prefPos = pos;
            }
        }
    }
}

/// <summary>
/// プレイヤーとの敵対関係の変化
/// </summary>
public class RelationshipHundler
{
    /// <summary>
    /// 値が変更された時にメッセージを流すプロパティ
    /// 値の読み取りと監視のみ公開する
    /// </summary>
    private ReactiveProperty<RelationshipType> rpCurrentRelationship = new ReactiveProperty<RelationshipType>();
    public IObservable<RelationshipType> OnRelationshipChanged { get { return rpCurrentRelationship; } }
    public RelationshipType CurrentRelationshop { get { return rpCurrentRelationship.Value; } }

    private EnemyData enemyData;

    public RelationshipHundler(EnemyData enemyData)
    {
        this.enemyData = enemyData;

        var player = GameRuleManager.Instance.Player;
        ChangeRelationship(player);

        //プレイヤーの外見が変わると、敵対関係が変化する
        GameRuleManager.OnPlayerAppearanceChanged += ChangeRelationship;
    }

    private void ChangeRelationship(Player player)
    {
        if (enemyData.Hostiles.Contains(player.CurrentOutfitType))
        {
            ChangeIntoHostile();
            return;
        }
        ChangeIntoFriendly();
    }

    void ChangeIntoHostile()
    {
        rpCurrentRelationship.Value = RelationshipType.Hostile;
    }

    void ChangeIntoFriendly()
    {
        rpCurrentRelationship.Value = RelationshipType.Friendly;
    }

}

public enum RelationshipType
{
    Hostile,
    Friendly
}
<file_sep>﻿using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

[DefaultExecutionOrder(-1)]
[RequireComponent(typeof(HumanController))]
public class Player : MonoBehaviour, IInteractive
{
    [SerializeField] OutfitData outfitData;
    [SerializeField] OutfitHundler outfitHundler;
    HumanController humanController;

    static readonly float rayRadius = 1.8f;
    public OutfitType CurrentOutfitType { get { return outfitHundler.CurrentOutfit.Type; } }

    public Action<InteractionType> OnInteractionUpdate;
    public Action OnInteractionExit;

    public bool IsDead { get { return humanController.IsDead; } }

    public IInteractive TargetInteraction { private set; get; }

    private void Awake()
    {
        outfitHundler.SetOutfit(outfitData,true);
        humanController = GetComponent<HumanController>();
        humanController.OnDisguise += () =>GameRuleManager.OnPlayerAppearanceChanged?.Invoke(this);
        humanController.OnDead += () => GameRuleManager.Instance.Gameover();
    }

    //球のレイを飛ばして
    private void Update()
    {
        int mask = 1 << (int)Layer.Interactive;
        var cols = Physics.SphereCastAll(transform.position, rayRadius, Vector3.up, 0.01f, mask);

        //ヒットしなかった
        if (cols.Length == 0)
        {
            //干渉先を見失った
            if (TargetInteraction != null)
            {
                TargetInteraction = null;
                OnInteractionExit?.Invoke();

            }
            return;
        }
        //近い順に並べて最初をとる
        var hit = cols.OrderBy(x => (x.transform.position - transform.position).sqrMagnitude).FirstOrDefault();

        var interactive = hit.collider.gameObject.GetComponent<IInteractive>();
        if (interactive == null) return;

        TargetInteraction = interactive;
        OnInteractionUpdate?.Invoke(TargetInteraction.GetInteractionType());

        return;

    }

    public Vector3 GetPosition()
    {
        return transform.position;
    }

    public InteractionType GetInteractionType()
    {
        return InteractionType.Kill;
    }

    public void Interact(InteractionType interactionType)
    {
        humanController.Interacted(interactionType);
    }

    public OutfitData GetDisguiseOutfit()
    {
        return outfitHundler.CurrentOutfit;
    }
}
<file_sep>﻿using System.Collections;
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;

namespace AKUtil
{
    /// <summary>
    /// SkinnedMeshRendererのボーン構造をコピーする便利クラスです。
    /// 既に存在するキャラクターのPrefabに、ボーン構造が同じ新しいスキンを追加したいときなどに使います。
    /// </summary>
    public static class SkinnedMeshRendererExtension
    {
        public static void CopyBones(Transform root, SkinnedMeshRenderer before, SkinnedMeshRenderer newMeshRenderer)
        {
            // update mesh
            var meshrenderer = before;
            meshrenderer.sharedMesh = newMeshRenderer.sharedMesh;
            Transform[] childrens = root.GetComponentsInChildren<Transform>(true);
            // sort bones.
            Transform[] bones = new Transform[newMeshRenderer.bones.Length];
            for (int boneOrder = 0; boneOrder < newMeshRenderer.bones.Length; boneOrder++)
            {
                bones[boneOrder] = Array.Find<Transform>(childrens, c => c.name == newMeshRenderer.bones[boneOrder].name);
            }
            meshrenderer.bones = bones;
        }
    }

}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx.Toolkit;

namespace AKUtil{
    public class ComponentPool<T> : ObjectPool<T>
    where T : Component
    {
        private readonly T prefab;
        private readonly Transform parentTransform;

        public ComponentPool(Transform parentTransform, T prefab)
        {
            this.parentTransform = parentTransform;
            this.prefab = prefab;
        }

        /// <summary>
        /// オブジェクトの追加生成時に実行される
        /// </summary>
        protected override T CreateInstance()
        {
            var e = GameObject.Instantiate(prefab);
            e.transform.SetParent(parentTransform);

            return e;
        }
    }

}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using AKUtil;
using UniRx;

/// <summary>
/// 移動部分だけの部分クラス
/// </summary>
public partial class HumanController : MonoBehaviour
{
    static readonly float directionThreshold = 0.1f;
    [SerializeField] float maxVelocity = 5f;
    [SerializeField] float curveVelocity = 0.5f;
    [SerializeField] float moveDrug = 10;
    [SerializeField] [Range(0, 1)] float angularSpeed = 1f;

    float prevVelocity;
    Vector3 nextVelocity;

    StateMachine stateMachine = new StateMachine();
    public State Idle = new State("Idle");
    public State Run = new State("Run");
    public State Walk = new State("Walk");

    partial void OnStart()
    {
        bool isConflict = false;
        //遷移の設定と被りのチェックを行う
        isConflict |= Idle > Walk;
        isConflict |= Idle > Run;
        isConflict |= Walk > Run;
        isConflict |= Walk > Idle;
        isConflict |= Run > Idle;
        isConflict |= Run > Walk;

        stateMachine.SetInitialState(Idle);
       //Assert.IsFalse(isConflict, "遷移が重複しています");

        Idle.OnStart.Subscribe(_ => OnStartIdle());
        Walk.OnStart.Subscribe(_ => OnStartWalk());
        Run.OnStart.Subscribe(_ => OnStartRun());
    }

    void OnStartIdle()
    {
        outfitHundler.Animator.CrossFade("Idle", 0.1f, 0, 0);
    }

    void OnStartRun()
    {
        outfitHundler.Animator.CrossFade("Run", 0.1f, 0, 0);
    }

    void OnStartWalk()
    {
        outfitHundler.Animator.CrossFade("Walk", 0.1f, 0, 0);
    }

    void UpdateVelocity()
    {
        if (input.IsLocked || IsDead)
        {
            nextVelocity = Vector3.zero;
            return;
        }
        var direction = input.Direction;
        var magnitude = direction.magnitude;

        //回転
        var newForward = new Vector3(direction.x, 0, direction.z).normalized;
        newForward = Vector3.Lerp(transform.forward, newForward, angularSpeed);
        transform.forward = newForward;

        if (magnitude <= 0.02f)
        {
            //指をはなしている時は速度を減衰させる
            prevVelocity = Mathf.Max(0, prevVelocity - moveDrug * Time.deltaTime);
            nextVelocity = transform.forward * prevVelocity;
            if (!stateMachine.NowStateIs(Idle)) stateMachine.PushState(Idle);
            return;
        }
        /*
        //回転の制御
        var angleY = Mathf.Atan2(direction.z, direction.x) * Mathf.Rad2Deg;
        angleY -= 90f;
        var newAngle = Quaternion.Euler(0f, -angleY, 0f);
        var rotation = Quaternion.Lerp(transform.rotation, newAngle, angularSpeed);
        var velocity = Mathf.Lerp(0,maxVelocity, direction.magnitude);

        rigidbody.MoveRotation(rotation);
        */
        var velocity = Mathf.Lerp(0, maxVelocity, direction.magnitude);
        velocity = Mathf.Min(prevVelocity + 0.5f, velocity);
        var velocityVector = transform.forward * velocity;
        velocityVector.y = rigidbody.velocity.y;

        nextVelocity = velocityVector;
        prevVelocity = velocity;

        if (magnitude <= 0.8f)
        {
            if (!stateMachine.NowStateIs(Walk)) stateMachine.PushState(Walk);
            return;
        }

        if (!stateMachine.NowStateIs(Run)) stateMachine.PushState(Run);
    }

    void UpdateRigidbody()
    {
        rigidbody.velocity = nextVelocity;
    }

    // Update is called once per frame
    void Update()
    {
        UpdateVelocity();
        //stateMachine.Update();
    }

    private void FixedUpdate()
    {
        UpdateRigidbody();
        //stateMachine.FixedUpdate();
    }

    private void LateUpdate()
    {
       //stateMachine.LateUpdate();
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "EnemyData", menuName = "ScriptableObjects/EnemyData")]
public class EnemyData : ScriptableObject
{
    public OutfitData OutfitData;
    public OutfitType[] Hostiles;
}<file_sep>﻿using System;
using System.Collections.Generic;
using UniRx;

namespace AKUtil
{
    public class State
    {
        public string Name { get; private set; }
        private Subject<Unit> StartSubject = new Subject<Unit>();
        private Subject<Unit> EndSubject = new Subject<Unit>();
        private Subject<Unit> UpdateSubject = new Subject<Unit>();
        private Subject<Unit> FixedUpdateSubject = new Subject<Unit>();
        private Subject<Unit> LateUpdateSubject = new Subject<Unit>();
        public IObservable<Unit> OnStart { get { return StartSubject; } }
        public IObservable<Unit> OnEnd { get { return EndSubject; } }
        public IObservable<Unit> OnUpdate { get { return UpdateSubject; } }
        public IObservable<Unit> OnFixedUpdate { get { return FixedUpdateSubject; } }
        public IObservable<Unit> OnLateUpdate { get { return LateUpdateSubject; } }
        HashSet<State> nextStates = new HashSet<State>();
        public State(string name)
        {
            Name = name;
        }
        public static bool operator >(State current, State next)
        {
            return !current.nextStates.Add(next);
        }
        public static bool operator <(State next, State prev)
        {
            return prev > next;
        }
        public bool CanShiftTo(State next)
        {
            return (this.nextStates.Contains(next));
        }

        public void Update()
        {
            UpdateSubject.OnNext(Unit.Default);
        }
        public void FixedUdate()
        {
            FixedUpdateSubject.OnNext(Unit.Default);
        }
        public void LateUpdate()
        {
            LateUpdateSubject.OnNext(Unit.Default);
        }

        public void Enter()
        {
            StartSubject.OnNext(Unit.Default);
        }

        public void Exit()
        {
            EndSubject.OnNext(Unit.Default);
        }
    }
}<file_sep># fake-them-all

暗殺がテーマの3Dゲームです。 <br>
変装して標的に近づき、誰にも気付かれずに抹殺することが目的です。 <br>

# プレイ動画
以下のサムネイルからプレイ動画に飛びます。<br>
[![](https://img.youtube.com/vi/dnfj7rQEJig/0.jpg)](https://www.youtube.com/watch?v=dnfj7rQEJig)

# 内容について

3Dモデル等はアセットを使用しているので、<br>
著作権の観点からスクリプトのみ公開しています。<br><br>
Unityの使い方だけでなく、オブジェクト指向やデザインパターンについての理解度を示すために公開しています。<br>
以下、コードの概要です。
- ステートマシンやオブジェクトプール、シングルトンの実装。
- 敵の種類について、「同じ見た目だけど違う挙動をする」等の設定を、ScriptableObjectの組み合わせだけで実現。
- 敵AIをステートマシンで実装。NavMeshの経路だけ取り出し、移動は自前で制御。
- 敵の視界を扇型に飛ぶレイで実装。レイの本数、扇の角度や半径を変更することで精度や視界の大きさを調節可能。
- プレイヤーと敵で共通する処理(移動やパンチ等)は別のクラスに書き出し、プレイヤーのInputクラスや敵AIの入力を利用して動かす。



# 使用技術
- Unity v2019.3.10f1
- C# 7.3
- .NET 4.X

<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class HumanAnimator : MonoBehaviour
{
    [SerializeField] Animator animator;

}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

/// <summary>
/// インプット
/// </summary>
public class SwipeController : IFTOInput
{
    [SerializeField] InteractButton button;

    static readonly float sensitivity = 0.0062f;

    Vector3 prevPos;
    Vector3 currentPos;

    private void Start()
    {
        button.OnClick += Interact;
        Direction = Vector3.zero;
    }

    void Update ()
    {
        if (IsLocked) return;

        UpdateMousePosition();
    }

    bool isHoldingAnyKey;

    void UpdateMousePosition()
    {
        var x = Input.GetAxis("Horizontal");
        var y = Input.GetAxis("Vertical");
        var space = Input.GetKeyDown(KeyCode.Space);
        var input = new Vector3(x, 0, y);
        if (input.sqrMagnitude >= 0.01f)
        {
            Direction = new Vector3(x, 0, y).normalized;
            isHoldingAnyKey = true;
        }

        if (input.sqrMagnitude < 0.01f && isHoldingAnyKey)
        {
            Direction = Vector3.zero;
            isHoldingAnyKey = false;
        }

        if (space)
        {
            Interact();
        }

        var onDown = Input.GetMouseButtonDown(0);
        var down = Input.GetMouseButton(0);
        var onUp = Input.GetMouseButtonUp(0);

        if (onDown)
        {
            prevPos = currentPos = Input.mousePosition;
            return;
        }

        if (down)
        {
            //prevPos = currentPos;
            currentPos = Input.mousePosition;
            UpdateController();
            return;
        }

        if (onUp)
        {
            prevPos = currentPos;
            Direction = Vector3.zero;
            return;
        }
    }

    void Interact()
    {
        if (button.TargetInteraction == null) return;
        interactionSubject.OnNext(button.TargetInteraction);
    }

    void Interact(IInteractive target)
    {
        if (target == null) return;
        interactionSubject.OnNext(target);
    }

    void UpdateController()
    {
        var delta = currentPos - prevPos;
        delta.z = delta.y;
        delta.y = 0;
        Direction = delta * sensitivity;
    }
}
<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;

/// <summary>
/// 人間の動作を共通化しているクラス
/// </summary>
[RequireComponent(typeof(OutfitHundler))]
[RequireComponent(typeof(Collider))]
public partial class HumanController : MonoBehaviour
{
    [SerializeField] IFTOInput input;
    [SerializeField] Rigidbody rigidbody;

    OutfitHundler outfitHundler;

    public Action OnDead;
    public Action OnDisguise;

    public bool IsDead { private set; get; } = false;

    partial void OnStart();

    void Start()
    {

        input.OnInteract.Subscribe(Interact);
        outfitHundler = GetComponent<OutfitHundler>();
        OnDead += () => IsDead = true;
        OnStart();
    }

    void Interact(IInteractive interactive)
    {
        var type = interactive.GetInteractionType();

        switch (type)
        {
            case InteractionType.Kill:
                Kill(interactive);
                break;
            case InteractionType.Disguise:
                Disguise(interactive);
                break;
            case InteractionType.Use:
                Poison();
                break;
            default:
                break;
        }

        interactive.Interact(type);
    }

    public void Interacted(InteractionType interactionType)
    {
        switch (interactionType)
        {
            case InteractionType.Kill:
                Killed();
                break;
            case InteractionType.Disguise:
                BeNaked();
                break;
            default:
                return;
        }
    }

    Coroutine coroutine;

    void Killed()
    {
        GetComponent<Collider>().isTrigger = true;
        StopCoroutine();
        if (IsDead) return;
        IsDead = true;
        OnDead?.Invoke();
        input.IsLocked = true;
        outfitHundler.Animator.CrossFade("Death", 0, 0, 0);
    }

    void StopCoroutine()
    {
        if (coroutine != null)
        {
            input.IsLocked = false;
            StopCoroutine(coroutine);
        }
    }

    void Kill(IInteractive target)
    {
        Debug.Log("kill");
        transform.LookAt(target.GetPosition());
        StopCoroutine();
        coroutine = StartCoroutine(KillCoroutine());
    }

    IEnumerator KillCoroutine()
    {
        input.IsLocked = true;
        outfitHundler.Animator.CrossFade("Punch", 0, 0, 0);
        yield return new WaitForSeconds(0.48f);
        outfitHundler.Animator.CrossFade("Idle", 0.1f, 0, 0);
        yield return new WaitForSeconds(0.1f);
        input.IsLocked = false;
    }

    void Disguise(IInteractive target)
    {
        StopCoroutine();
        coroutine = StartCoroutine(DisguiseCoroutine(target));
    }

    IEnumerator DisguiseCoroutine(IInteractive target)
    {
        var outfit = target.GetDisguiseOutfit();
        if (outfit == null) yield break;

        input.IsLocked = true;
        outfitHundler.Animator.CrossFade("Disguise", 0, 0, 0);
        yield return new WaitForSeconds(4f/3);
        outfitHundler.SetOutfit(outfit);
        outfitHundler.Animator.CrossFade("Disguise", 0, 0, 0);
        outfitHundler.Animator.CrossFade("Idle", 0.1f, 0, 0);
        yield return new WaitForSeconds(0.1f);
        OnDisguise?.Invoke();
        input.IsLocked = false;

    }

    void Poison()
    {
        input.IsLocked = true;
        outfitHundler.Animator.CrossFade("Stir", 0, 0, 0);
    }


    void BeNaked()
    {
        StopCoroutine();
        coroutine = StartCoroutine(BeNakedCoroutine());
    }

    IEnumerator BeNakedCoroutine()
    {
        yield return new WaitForSeconds(4f / 3);

        outfitHundler.BeNaked();
        outfitHundler.Animator.CrossFade("Idle_Death", 0, 0, 0);
    }

}<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using AKUtil;

public class DatasetLocator : SingletonMonoBehaviour<DatasetLocator>
{
    [SerializeField] VisionMaterialData visionMaterialData;
    [SerializeField] OutfitData nakedOutfitData;

    public VisionMaterialData VisionMaterialData { get { return visionMaterialData; } }
    public OutfitData NakedOutfitData { get { return nakedOutfitData; } }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(OutfitLimitedInteraction))]
public class PoisonFinisher : MonoBehaviour
{
    private void Start()
    {
        GetComponent<OutfitLimitedInteraction>().OnInteracted += GameClear;
    }

    void GameClear()
    {
        GameRuleManager.Instance.PlayPoisonFinisher();
    }
}
<file_sep>﻿using UnityEditor;

namespace AKUtil
{
    public static class ToggleGameObjectActiveKeyboardShortcut
    {
        public static bool IsAvailable()
        {
            return Selection.activeGameObject != null;
        }

        // エスケープのために適当な大文字を使っているが実際はアンダーバーで起動する。
#if UNITY_EDITOR_OSX
        [MenuItem("AKUtil/Toggle GameObject ActiveSelf _A_",priority = 610)]
#else
        [MenuItem("AKUtil/Toggle GameObject ActiveSelf _¥¥",priority = 410)]
#endif
        public static void Execute()
        {
            foreach (var go in Selection.gameObjects) {
                Undo.RecordObject(go, go.name + ".activeSelf");
                go.SetActive(!go.activeSelf);
            }
        }
    }
}<file_sep>﻿using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Assertions;
using UniRx;

namespace AKUtil
{
    /// <summary>
    /// プッシュダウンステートマシン
    /// 過去の状態を保存しておくことができる。
    /// </summary>
    public class StateMachine
    {
        //変更の監視と値の読み取りだけ公開する
        private ReactiveProperty<State> rpCurrentState = new ReactiveProperty<State>();
        public State CurrentState { private set{ rpCurrentState.Value = value; } get { return rpCurrentState.Value; } }
        public IObservable<State> OnStateChanged { get { return rpCurrentState; } }

        Stack<State> stateStack;

        public StateMachine()
        {
            stateStack = new Stack<State>();
        }

        public void SetInitialState(State next)
        {
            stateStack.Push(next);
            CurrentState = next;
            CurrentState.Enter();
        }

        public void Update()
        {
            CurrentState.Update();
        }

        public void FixedUpdate()
        {
            CurrentState.FixedUdate();
        }

        public void LateUpdate()
        {
            CurrentState.LateUpdate();
        }

        public void PushState(State next)
        {
#if UNITY_EDITOR
            //Debug.Log(CurrentState.Name + " -> " + next.Name);
#endif
            var canShift = CurrentState.CanShiftTo(next);
            Assert.IsTrue(canShift, "その遷移は設定されていません");
            if (!canShift) return;

            CurrentState.Exit();
            stateStack.Push(next);
            CurrentState = next;
            CurrentState.Enter();
        }

        public bool PopState()
        {
            var canPop = stateStack.Count <= 1;
            Assert.IsTrue(canPop, "ステートがスタックされていません");
            if (!canPop) return false;

            stateStack.Pop();
            CurrentState.Exit();
            CurrentState = stateStack.Peek();
            CurrentState.Enter();

            return true;
        }

        public bool NowStateIs(State dest)
        {
            return CurrentState == dest;
        }
    }
}<file_sep>﻿namespace AKUtil
{
    public static class GameLayer
    {
        /// <summary>
        /// 全てのレイヤーと衝突するレイヤーマスク
        /// </summary>
        /// <returns>The all collision mask.</returns>
        /// <param name="targetLayers">Target layers.</param>
        public static int GetAllCollisionLayerMask(this Layer[] targetLayers)
        {
            int layerMask = 0;
            foreach (var layer in targetLayers)
            {
                layerMask |= 1 << (int)layer;
            }
            return layerMask;
        }
    }
}

public enum Layer
{
    Player = 8,
    Interactive = 9
}<file_sep>﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UniRx;
using AKUtil;

/// <summary>
/// レイにより視界を設定し、指定されたレイヤーのプレイヤーが入ったら通知する。
/// </summary>
public class CharacterDetector : MonoBehaviour
{
    [SerializeField] CircularSectorMeshRenderer circleRenderer;
    [SerializeField] float searchDistance = 0.5f;
    [SerializeField] float angle = 90;
    [SerializeField] float rayIntervalAngle = 1f;  //何度ごとにレイを飛ばすか
    [SerializeField] Layer[] targetLayers;  //レイキャストのターゲット

    Vector3[] rayDirections;
    VisionMaterialData materialData;
    RelationshipHundler relationshipHundler;
    Coroutine colorChangeCoroutine;

    private Subject<Player> DetectSubject = new Subject<Player>();
    private Subject<Unit> LostSubject = new Subject<Unit>();

    public IObservable<Player> OnDetect { get { return DetectSubject; } }
    public IObservable<Unit> OnLost { get { return LostSubject; } }

    public bool IsActive { private set; get; } = true;
    bool isInVision = false;
    internal bool IsInvision { get { return isInVision; } }

    //関係が変わった時に色を変える
    public void SetRelationship(RelationshipHundler relationshipHundler)
    {
        this.relationshipHundler = relationshipHundler;
        ChangeColorImmediately();
        relationshipHundler.OnRelationshipChanged
            .Subscribe(_ => ChangeColor());
    }

    public void Sleep()
    {
        IsActive = false;
        circleRenderer.gameObject.SetActive(false);
    }

    void Awake()
    {
        SetupCircleRenderer();
        SetupRay();
        OnLost.Subscribe(_ => ChangeColorImmediately());
    }

    void ChangeColorImmediately()
    {
        var mat = GetVisionMaterial(relationshipHundler.CurrentRelationshop);
        circleRenderer.SetMaterial(mat);
    }
    void ChangeColor() {
        if (colorChangeCoroutine != null) StopCoroutine(colorChangeCoroutine);
        colorChangeCoroutine = StartCoroutine(ChangeVisionColorCoroutine());
    }

    //追従中にプレイヤーが着替えたりしても、色をそのままにする。
    IEnumerator ChangeVisionColorCoroutine()
    {
        while (isInVision)
        {
            yield return null;
        }

        ChangeColorImmediately();
    }

    private Material GetVisionMaterial(RelationshipType relation)
    {
        bool isHostileAndDetect = relation == RelationshipType.Hostile && IsInvision;
        bool isHostileAndNotDetect = relation == RelationshipType.Hostile && !IsInvision;
        bool isFriendly = relation == RelationshipType.Friendly;

        if (isHostileAndDetect) return materialData.detectedMaterial;

        if (isHostileAndNotDetect) return materialData.dangerMaterial;

        if (isFriendly) return materialData.safeMaterial;

        return materialData.safeMaterial;
    }

    [ContextMenu("SetupVision")]
    void SetupCircleRenderer()
    {
        materialData = DatasetLocator.Instance.VisionMaterialData;

        circleRenderer.degree = angle;
        circleRenderer.radius = searchDistance;
        //扇型の視界がまっすぐに見えるよう調整する
        float baseAngleOffset = 90f - angle / 2;
        circleRenderer.beginOffsetDegree = baseAngleOffset;
    }

    void SetupRay()
    {
        //レイ方向を作成する
        int rayCount = Mathf.FloorToInt(angle / rayIntervalAngle) + 1;

        rayDirections = new Vector3[rayCount];
        float baseAngleOffset = 90f - angle / 2;
        for (int i = 0; i < rayCount; i++)
        {
            var offset = baseAngleOffset + i * rayIntervalAngle;
            var vector = new Vector3(1, 0, 0) * searchDistance;
            vector = Quaternion.Euler(0, offset + 180, 0) * vector;
            rayDirections[i] = vector;
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (!IsActive) return;
        var hits = RaycastAll();
        bool detectedNone = hits.Count == 0;

        //視界に何も写っていない
        if (detectedNone) return;

        //何か見つけた
        foreach (var hit in hits)
        {
            var player = hit.collider.gameObject.GetComponent <Player>();
            if (player == null) continue;
            if (player.IsDead) return;
            if (relationshipHundler.CurrentRelationshop == RelationshipType.Friendly) return;
            DetectSubject.OnNext(player);
            isInVision = true;
            ChangeColorImmediately();
            return;
        }

        //視界から消えた
        if (isInVision)
        {
            isInVision = false;
            LostSubject.OnNext(Unit.Default);
            return;
        }

        isInVision = false;
    }

    List<RaycastHit> RaycastAll()
    {
        List<RaycastHit> hits = new List<RaycastHit>();
        foreach(var direction in rayDirections)
        {
            Vector3 rotDir = transform.rotation * direction;
            var ray = new Ray(transform.position, rotDir);
            RaycastHit hit;
            //int mask = targetLayers.GetAllCollisionLayerMask();
            int mask = ~(1 << (int)Layer.Interactive);
            if (Physics.Raycast(transform.position + Vector3.up * 0.3f,rotDir,out hit,searchDistance,mask))hits.Add(hit);
        }
        return hits;
    }

    /// <summary>
    /// ギズモに経路を表示
    /// </summary>
    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        var prefPos = transform.position;

        if (rayDirections == null) SetupRay();
        foreach (var direction in rayDirections)
        {
            var rotDir = transform.rotation * direction;
            Gizmos.DrawLine(prefPos, prefPos + rotDir);
        }
    }
}
<file_sep>﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UniRx.Toolkit;
using AKUtil;

public class ParticlePool : MonoBehaviour
{

    ComponentPool<ParticlePool> pool;
    public ParticleSystem[] particles;
    Transform target;
    float time;
    float deleteTime;

    bool isFirstStart = true;

    void OnEnable()
    {
        if (!isFirstStart) return;
        isFirstStart = false;
        if (particles[0] == null) particles[0] = GetComponent<ParticleSystem>();
    }

    void Update()
    {
        if (time > deleteTime)
        {
            pool?.Return(this);
        }
        time += Time.deltaTime;
        if (target != null) this.transform.position = target.transform.position;
    }

    public void Play(float deleteTime, ComponentPool<ParticlePool> pool, Transform target)
    {
        if (particles[0] == null)
        {
            particles[0] = GetComponent<ParticleSystem>();
        }
        time = 0;
        this.deleteTime = deleteTime;
        this.pool = pool;
        this.target = target;

        foreach (var particle in particles)
        {
            particle.Stop();
            particle.Play();
        }
    }
}
