iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
Software Development

Vibe Unity - AI時代的遊戲開發工作流系列 第 13

Day 13 - 北極熊走來走去, 發呆, 睡覺的功能

  • 分享至 

  • xImage
  •  

這一章, 我們會實現最後一個功能 - 北極熊自己在場景中走來走去, 發呆, 睡覺的功能

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);
	}
}



image.png

我們可以先在北極熊身上增加 PetAIController 的代碼
並且填入對於的動畫名稱
如果你還沒有, 可以先新增或是先填入已經有的動畫當作預設的:

image.png

接下來我們還需要增加一個北極熊可以移動的範圍

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;
	}
}



image.png

新增一個新的Game Object, 並且把可以移動的範圍大小設定好
然後再回到北極熊身上, 把這個 PetMoveArea 的物件拉到 Inspector 的欄位中


iShot_2025-09-23_23.20.29.mp4

這樣一來, 我們就把基本的寵物互動遊戲功能開發好啦
欸 ? 但是 AI 互動, 對話的功能呢 ?
別急, 在開始實作 AI 互動的功能之前
我們接下來先花一點時間來介紹各種 AI 平台的 API 要怎麼串接和使用吧 ~


上一篇
Day 12 - 實現可以拖拉道具出來的 UI 欄位
系列文
Vibe Unity - AI時代的遊戲開發工作流13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言