Unity人間生活 – 3

到着してから「棒立ち」ではなく、ちゃんとベッドに横になったり、椅子に座ったりすると、一気にシミュレーションのリアリティが増します。

これを実現するには、**「家具側にアニメーションの種類と『位置合わせ』の情報を待たせること」**が重要です。

以下に、アニメーションと位置合わせ(Snapping)を組み込んだ実装手順を解説します。


1. 準備:アニメーションの状態管理 (Animator)

まず、UnityのAnimator Controllerで、以下のようなステート(状態)とパラメータを作成する必要があります。

必要なパラメータ:

  • Speed (Float): 移動用(0なら止まる、0.1以上なら歩く)
  • IsSleeping (Bool): 寝ているかどうか
  • IsSitting (Bool): 座っているかどうか
  • DoEat (Trigger): 食べる動作(1回再生)

ステートマシンの構造イメージ:

Plaintext

[Entry] -> [Locomotion (Idle/Walk)]
             |      ^
             v      |
        [Sleep State] (IsSleeping = true で遷移)
             |      ^
             v      |
        [Sit State]   (IsSitting = true で遷移)
             |      ^
             v      |
        [Eat State]   (DoEat Trigger で遷移)

※ アニメーションデータ(.animファイル)がない場合は、Adobeの無料サービス「Mixamo」から「Idle」「Walking」「Sleeping」「Sitting」「Eating」などをダウンロードして使うのが一般的です。


2. コードの更新

前回のスクリプトをアップグレードします。

A. 家具用スクリプト (SmartObject.cs)

家具に「ここでどんなアニメをするか」と「キャラクターがどこに位置合わせすべきか」という情報を追加します。

C#

using UnityEngine;

public enum NeedType { None, Hunger, Energy, Fun }

// 追加: アニメーションの種類
public enum ActionAnimation { None, Eat, Sleep, Sit }

public class SmartObject : MonoBehaviour
{
    [Header("基本設定")]
    public string objectName = "Furniture";
    public NeedType needType;
    public float recoveryAmount = 50f;
    public float interactionTime = 3f;

    [Header("アニメーション設定")]
    public ActionAnimation actionType; // この家具で何のアニメをするか
    public Transform interactionPoint; // 追加: 座る/寝る時の正確な位置と向き

    // 呼び出し用の位置取得
    public Vector3 GetInteractionPosition()
    {
        // interactionPointが設定されていればその位置、なければ家具の位置
        return interactionPoint != null ? interactionPoint.position : transform.position;
    }
}

B. AI用スクリプト (HumanLifeAI.cs)

NavMeshAgentを一時停止させ、位置を家具にスナップ(吸着)させてからアニメーションを再生します。

C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
using System.Linq;

public class HumanLifeAI : MonoBehaviour
{
    // ... (欲求パラメータ変数は前回と同じなので省略) ...
    [Header("欲求パラメータ (0-100)")]
    public float hunger = 100f;
    public float energy = 100f;
    public float fun = 100f;
    
    // 省略... (Decay変数など)

    private NavMeshAgent agent;
    private Animator animator; // 追加: アニメーター
    private bool isBusy = false;

    void Start()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponent<Animator>(); // コンポーネント取得
    }

    void Update()
    {
        // 移動アニメーションの制御
        float speed = agent.velocity.magnitude;
        animator.SetFloat("Speed", speed); // Speedパラメータに速度を渡す

        // ... (欲求減少とThinkAndActロジックは前回と同じ) ...
        DecreaseNeeds();
        if (!isBusy) ThinkAndAct();
    }

    // ... (ThinkAndAct, FindAndInteract, Wander などは前回と同じ) ...
    // ... 下記の ExecuteAction だけ書き換えてください ...

    // ★ 更新箇所: アニメーション対応版のアクション実行
    IEnumerator ExecuteAction(SmartObject target)
    {
        isBusy = true;
        agent.SetDestination(target.GetInteractionPosition());

        // 移動中待機
        while (agent.pathPending || agent.remainingDistance > agent.stoppingDistance)
        {
            yield return null;
        }

        // 1. 到着したらNavMeshAgentの制御を一時的に切る(位置合わせのため)
        agent.isStopped = true;
        
        // 2. 位置と回転の補正 (Snapping)
        // ベッドや椅子の場合、正確な位置と向きにキャラを移動させる
        if (target.interactionPoint != null)
        {
            // 位置を合わせる(少し時間をかけて補間するとより綺麗ですが、今回は瞬時に)
            transform.position = target.interactionPoint.position;
            transform.rotation = target.interactionPoint.rotation;
        }

        // 3. アニメーション再生
        PlayActionAnimation(target.actionType, true);

        // 4. 行動時間待機
        yield return new WaitForSeconds(target.interactionTime);

        // 5. アニメーション終了
        PlayActionAnimation(target.actionType, false);
        
        // 6. 回復処理とAgent再開
        RestoreNeed(target.needType, target.recoveryAmount);
        agent.isStopped = false; // 移動再開許可
        isBusy = false;
    }

    // アニメーターへの命令をまとめた関数
    void PlayActionAnimation(ActionAnimation type, bool isActive)
    {
        switch (type)
        {
            case ActionAnimation.Sit:
                animator.SetBool("IsSitting", isActive);
                break;
            case ActionAnimation.Sleep:
                animator.SetBool("IsSleeping", isActive);
                break;
            case ActionAnimation.Eat:
                if (isActive) animator.SetTrigger("DoEat"); 
                // Triggerは一度引けばいいのでfalse時は何もしない
                break;
        }
    }
    
    // ... (RestoreNeed, Wander, DecreaseNeeds は前回と同じ) ...
    void RestoreNeed(NeedType type, float amount)
    {
        switch (type)
        {
            case NeedType.Hunger: hunger = Mathf.Clamp(hunger + amount, 0, 100); break;
            case NeedType.Energy: energy = Mathf.Clamp(energy + amount, 0, 100); break;
            case NeedType.Fun: fun = Mathf.Clamp(fun + amount, 0, 100); break;
        }
    }

    void ThinkAndAct()
    {
         if (hunger < 30) FindAndInteract(NeedType.Hunger);
         else if (energy < 20) FindAndInteract(NeedType.Energy);
         else if (fun < 40) FindAndInteract(NeedType.Fun);
         else Wander();
    }
    
    void FindAndInteract(NeedType type)
    {
        SmartObject[] allObjects = FindObjectsOfType<SmartObject>();
        var targetObjects = allObjects.Where(obj => obj.needType == type).ToList();

        if (targetObjects.Count > 0)
        {
            SmartObject nearest = targetObjects.OrderBy(o => Vector3.Distance(transform.position, o.transform.position)).First();
            StartCoroutine(ExecuteAction(nearest));
        }
        else
        {
            Wander();
        }
    }
    
    void Wander()
    {
        if (agent.remainingDistance > agent.stoppingDistance) return;
        Vector3 randomDirection = Random.insideUnitSphere * 5f;
        randomDirection += transform.position;
        NavMeshHit hit;
        if (NavMesh.SamplePosition(randomDirection, out hit, 5f, 1)) agent.SetDestination(hit.position);
    }

    void DecreaseNeeds()
    {
        hunger -= 5f * Time.deltaTime;
        energy -= 2f * Time.deltaTime;
        fun -= 3f * Time.deltaTime;
    }
}

3. Unityエディタでの設定ポイント(ここが重要)

コードを書いただけでは綺麗に動きません。シーン上の設定が必要です。

Step 1: Interaction Point (位置合わせポイント) を作る

ベッドや椅子の子オブジェクトとして、空のGameObjectを作成し、名前を「Point」などにします。

  • ベッドの場合:
    • その「Point」を、枕の上に配置します。
    • 重要: PointのZ軸(青い矢印)が、キャラの足の向く方向(または頭の方向、アニメーションによる)になるように回転させます。寝るアニメーションが再生されたとき、このPointと同じ向きになります。
    • SmartObject コンポーネントの Interaction Point 欄に、この「Point」オブジェクトをドラッグ&ドロップします。

Step 2: Smart Object の設定

  • ベッド: Action Type を Sleep に設定。
  • 椅子: Action Type を Sit に設定。
  • 冷蔵庫: Action Type を Eat に設定(Interaction Pointは冷蔵庫の前の床に配置し、冷蔵庫の方を向くように回転させる)。

Step 3: Animator の遷移設定

Animatorウィンドウで、矢印(Transition)の設定を確認します。

  • Any State -> Sleep: Condition に IsSleeping = true を追加。Has Exit Time のチェックを外す(即座に寝るため)。
  • Sleep -> Locomotion: Condition に IsSleeping = false を追加。
  • Locomotion -> Sit: Condition に IsSitting = true
  • Sit -> Locomotion: Condition に IsSitting = false

これでどうなる?

実行すると、キャラクターは以下のように動きます。

  1. 体力が減る。
  2. ベッドへ歩いていく。
  3. ベッドの横に着くと、スッと枕の上にワープし、体の向きを整える。
  4. 「寝るアニメーション」が再生され、動かなくなる。
  5. 体力が回復した後、むっくり起き上がり(Idleに戻り)、次の行動(冷蔵庫など)へ向かう。

これで一気に「シム」っぽい挙動になります。