這一章, 我們會實現最後一個功能 - 北極熊自己在場景中走來走去, 發呆, 睡覺的功能
Prompt:
```javascript
實作代碼: 北極熊會在場景中自己走來走去, 偶爾睡覺, 偶爾發呆
1. 每個動作有自己的 Animation Clip
2. 與現有的代碼整合起來
3. 拖拽, 吃東西, 搓澡的時候不移動
4. 限定移動的範圍區域, 在Editor Draw Gizmoz 繪製可移動範圍
```
Code:
Cursor 生成了兩個代碼,
一個是用來控制北極熊的動作的,
一個是用來限制北極熊的移動範圍的
PetAIController:
using UnityEngine;
/// <summary>
/// 北極熊寵物的簡易 AI:在指定區域內隨機走動、偶爾發呆與睡覺。
/// 整合互動/拖拽邏輯:當寵物被拖拽、進行互動(吃、洗澡)時不自動移動。
/// 動畫狀態:Idle、Walk、Sleep、Stare(可於 Inspector 指定對應動畫名)。
/// 設計重點:
/// - 盡量避免每幀開銷,僅在需要時更新方向與目標;
/// - 使用協程/計時器管理狀態停留時間;
/// - 使用 SpriteRenderer.flipX 控制朝向;
/// - 與 PetMoveArea 協作限制移動範圍並繪製 Gizmos。
/// </summary>
[RequireComponent(typeof(Animator))]
public class PetAIController : MonoBehaviour
{
[Header("References")]
[SerializeField] private Animator animator;
[SerializeField] private SpriteRenderer spriteRenderer;
[SerializeField] private PetMoveArea moveArea;
[SerializeField] private PetInteractionState interactionState;
[SerializeField] private PetDragController dragController;
[Header("Animation Names")]
[SerializeField] private string idleStateName = "Idle";
[SerializeField] private string walkStateName = "Walk_bear";
[SerializeField] private string sleepStateName = "Sleep";
[SerializeField] private string stareStateName = "Stare";
[Header("Movement")]
[Tooltip("每秒移動速度(世界單位/秒)")]
[SerializeField] private float moveSpeed = 1.2f;
[Tooltip("距離目標點小於此值視為抵達,避免抖動")]
[SerializeField] private float arriveDistance = 0.05f;
[Tooltip("Z 軸固定值(確保排序層級),若不需要可維持 0")]
[SerializeField] private float fixedWorldZ = 0f;
[Header("State Durations (Seconds)")]
[Tooltip("發呆時長範圍(含端點)")]
[SerializeField] private Vector2 stareDurationRange = new Vector2(1.5f, 4f);
[Tooltip("睡覺時長範圍(含端點)")]
[SerializeField] private Vector2 sleepDurationRange = new Vector2(4f, 9f);
[Tooltip("連續行走時長範圍(含端點)")]
[SerializeField] private Vector2 walkDurationRange = new Vector2(2f, 6f);
private enum AiState { Idle, Walk, Stare, Sleep }
private AiState currentState = AiState.Idle;
private float stateTimer;
private Vector3 walkTarget;
/// <summary>
/// 快取依賴並進入初始狀態
/// </summary>
private void Awake()
{
if (animator == null) animator = GetComponentInChildren<Animator>();
if (spriteRenderer == null) spriteRenderer = GetComponentInChildren<SpriteRenderer>();
if (interactionState == null) interactionState = GetComponent<PetInteractionState>();
if (dragController == null) dragController = GetComponent<PetDragController>();
if (moveArea == null) moveArea = FindObjectOfType<PetMoveArea>();
}
private void OnEnable()
{
SwitchToIdle();
}
private void Update()
{
// 若正在互動或被拖拽,暫停 AI(保持當前動畫)
if ((interactionState != null && interactionState.IsInteracting) ||
(dragController != null && dragController.IsDragging))
{
return;
}
// 狀態機計時與行為
switch (currentState)
{
case AiState.Walk:
TickWalk();
break;
case AiState.Stare:
TickCountdownThenRandomNext();
break;
case AiState.Sleep:
TickCountdownThenRandomNext();
break;
case AiState.Idle:
TickCountdownThenRandomNext();
break;
}
}
/// <summary>
/// 行走邏輯:朝目標點移動;抵達或超時後換下一個狀態
/// </summary>
private void TickWalk()
{
Vector3 pos = transform.position;
Vector3 target = walkTarget;
target.z = fixedWorldZ;
if (moveArea != null)
{
target = moveArea.ClampToArea(target);
}
Vector3 next = Vector3.MoveTowards(pos, target, moveSpeed * Time.deltaTime);
transform.position = new Vector3(next.x, next.y, fixedWorldZ);
// Flip 朝向
if (spriteRenderer != null)
{
float dx = target.x - pos.x;
if (Mathf.Abs(dx) > 0.001f)
{
spriteRenderer.flipX = dx < 0f;
}
}
// 抵達目標
if (Vector2.Distance(new Vector2(transform.position.x, transform.position.y), new Vector2(target.x, target.y)) <= arriveDistance)
{
// 在行走後隨機進入 Idle/發呆/睡覺
SwitchToRandomRestState();
}
else
{
stateTimer -= Time.deltaTime;
if (stateTimer <= 0f)
{
// 行走逾時也切換
SwitchToRandomRestState();
}
}
}
private void TickCountdownThenRandomNext()
{
stateTimer -= Time.deltaTime;
if (stateTimer <= 0f)
{
SwitchToWalk();
}
}
private void SwitchToIdle()
{
currentState = AiState.Idle;
stateTimer = Random.Range(stareDurationRange.x, stareDurationRange.y);
PlayAnimationSafe(idleStateName);
}
private void SwitchToStare()
{
currentState = AiState.Stare;
stateTimer = Random.Range(stareDurationRange.x, stareDurationRange.y);
PlayAnimationSafe(stareStateName);
}
private void SwitchToSleep()
{
currentState = AiState.Sleep;
stateTimer = Random.Range(sleepDurationRange.x, sleepDurationRange.y);
PlayAnimationSafe(sleepStateName);
}
private void SwitchToWalk()
{
currentState = AiState.Walk;
stateTimer = Random.Range(walkDurationRange.x, walkDurationRange.y);
PickNewWalkTarget();
PlayAnimationSafe(walkStateName);
}
private void SwitchToRandomRestState()
{
float r = Random.value;
if (r < 0.2f)
{
SwitchToSleep();
}
else if (r < 0.55f)
{
SwitchToStare();
}
else
{
SwitchToIdle();
}
}
private void PickNewWalkTarget()
{
if (moveArea != null)
{
walkTarget = moveArea.GetRandomWorldPoint(fixedWorldZ);
}
else
{
// 無區域時,以當前位置附近小半徑挑點
Vector2 rnd = Random.insideUnitCircle * 1.5f;
walkTarget = new Vector3(transform.position.x + rnd.x, transform.position.y + rnd.y, fixedWorldZ);
}
}
private void PlayAnimationSafe(string stateName)
{
if (animator == null) return;
if (string.IsNullOrEmpty(stateName)) return;
animator.Play(stateName);
}
}
我們可以先在北極熊身上增加 PetAIController 的代碼
並且填入對於的動畫名稱
如果你還沒有, 可以先新增或是先填入已經有的動畫當作預設的:
接下來我們還需要增加一個北極熊可以移動的範圍
PetMoveArea:
using UnityEngine;
/// <summary>
/// 定義寵物可移動的矩形範圍,並在 Editor 中以 Gizmos 繪製。
/// - 可指定世界座標中心與尺寸;
/// - 提供 Clamp 與隨機點取得;
/// - 建議與 PetAIController 搭配,限制北極熊隨機走動範圍。
/// </summary>
public class PetMoveArea : MonoBehaviour
{
[Header("Area (World Space)")]
[Tooltip("可移動區域中心(世界座標)")]
[SerializeField] private Vector2 areaCenter = Vector2.zero;
[Tooltip("可移動區域尺寸(世界單位)")]
[SerializeField] private Vector2 areaSize = new Vector2(8f, 4f);
[Tooltip("在 Scene 檢視中繪製的顏色(選中時)")]
[SerializeField] private Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.35f);
/// <summary>
/// 取得區域矩形(世界座標)
/// </summary>
public Rect GetWorldRect()
{
Vector2 size = areaSize;
Vector2 min = areaCenter - size * 0.5f;
return new Rect(min, size);
}
/// <summary>
/// 將世界座標點夾到可移動範圍內
/// </summary>
public Vector3 ClampToArea(Vector3 worldPosition)
{
Rect r = GetWorldRect();
float x = Mathf.Clamp(worldPosition.x, r.xMin, r.xMax);
float y = Mathf.Clamp(worldPosition.y, r.yMin, r.yMax);
return new Vector3(x, y, worldPosition.z);
}
/// <summary>
/// 取得區域內的隨機世界座標點
/// </summary>
public Vector3 GetRandomWorldPoint(float fixedZ)
{
Rect r = GetWorldRect();
float x = Random.Range(r.xMin, r.xMax);
float y = Random.Range(r.yMin, r.yMax);
return new Vector3(x, y, fixedZ);
}
/// <summary>
/// 在 Scene 視窗中繪製可視化矩形範圍
/// </summary>
private void OnDrawGizmosSelected()
{
Color old = Gizmos.color;
Gizmos.color = gizmoColor;
Rect r = GetWorldRect();
Vector3 center = new Vector3(r.center.x, r.center.y, 0f);
Vector3 size3 = new Vector3(r.size.x, r.size.y, 0f);
Gizmos.DrawWireCube(center, size3);
Gizmos.color = new Color(gizmoColor.r, gizmoColor.g, gizmoColor.b, gizmoColor.a * 0.15f);
Gizmos.DrawCube(center, size3);
Gizmos.color = old;
}
}
新增一個新的Game Object, 並且把可以移動的範圍大小設定好
然後再回到北極熊身上, 把這個 PetMoveArea 的物件拉到 Inspector 的欄位中
這樣一來, 我們就把基本的寵物互動遊戲功能開發好啦
欸 ? 但是 AI 互動, 對話的功能呢 ?
別急, 在開始實作 AI 互動的功能之前
我們接下來先花一點時間來介紹各種 AI 平台的 API 要怎麼串接和使用吧 ~