using System; using UnityEngine; namespace HotUpdate.Gameplay { /// /// 3D 营地主角探险家控制器:响应点击路面/建筑、平滑移动至目标位置并驱动待机与行走动画,支持到达事件与地面指示光圈。 /// public sealed class CampExplorerController : MonoBehaviour { [Header("Movement Settings")] public float moveSpeed = 4.8f; public float turnSpeed = 720f; public float stopDistance = 0.35f; [Header("Ground Boundaries")] public float minX = -10f; public float maxX = 10f; public float minZ = -14f; public float maxZ = 12f; Animator _animator; Vector3 _targetPosition; bool _isMoving; int _animParamIsMoving; int _animParamSpeed; Action _onArrival; Vector3? _targetFacing; GameObject _indicator; public bool IsMoving => _isMoving; public Vector3 TargetPosition => _targetPosition; public Animator ExplorerAnimator => _animator; void Awake() { EnsureInit(); EnsureIndicator(); } public void EnsureInit() { if (_animator == null) _animator = GetComponent(); if (_animParamIsMoving == 0) _animParamIsMoving = Animator.StringToHash("isMoving"); if (_animParamSpeed == 0) _animParamSpeed = Animator.StringToHash("speed"); } public void EnsureIndicator() { if (_indicator != null) return; Transform parentTf = transform.parent != null ? transform.parent : transform; var existing = parentTf.Find("ClickIndicator"); if (existing != null) { _indicator = existing.gameObject; return; } _indicator = new GameObject("ClickIndicator"); _indicator.transform.SetParent(parentTf, false); _indicator.layer = gameObject.layer; var mf = _indicator.AddComponent(); var mr = _indicator.AddComponent(); mf.sharedMesh = CreateRingMesh(0.85f, 1.25f, 32); var smr = GetComponentInChildren(); if (smr != null && smr.sharedMaterial != null) { mr.sharedMaterial = smr.sharedMaterial; } _indicator.SetActive(false); } static Mesh CreateRingMesh(float innerRadius, float outerRadius, int segments) { var mesh = new Mesh { name = "RingMesh" }; var verts = new Vector3[segments * 2]; var uvs = new Vector2[segments * 2]; var tris = new int[segments * 6]; float angleStep = 360f / segments * Mathf.Deg2Rad; for (int i = 0; i < segments; i++) { float a = i * angleStep; float cos = Mathf.Cos(a); float sin = Mathf.Sin(a); verts[i * 2] = new Vector3(cos * innerRadius, 0.04f, sin * innerRadius); verts[i * 2 + 1] = new Vector3(cos * outerRadius, 0.04f, sin * outerRadius); uvs[i * 2] = new Vector2((float)i / segments, 0); uvs[i * 2 + 1] = new Vector2((float)i / segments, 1); int next = (i + 1) % segments; int baseIdx = i * 6; tris[baseIdx] = i * 2; tris[baseIdx + 1] = next * 2; tris[baseIdx + 2] = i * 2 + 1; tris[baseIdx + 3] = next * 2; tris[baseIdx + 4] = next * 2 + 1; tris[baseIdx + 5] = i * 2 + 1; } mesh.vertices = verts; mesh.uv = uvs; mesh.triangles = tris; mesh.RecalculateNormals(); return mesh; } /// /// 指挥探险家移动到指定世界坐标(单参数重载,保持反射与旧接口兼容)。 /// public void MoveTo(Vector3 worldPos) => MoveTo(worldPos, null, null); /// /// 指挥探险家移动到指定世界坐标(自动对齐路面并做边界 Clamp)。 /// 可选指定到达回调与到达时的最终朝向。 /// public void MoveTo(Vector3 worldPos, Action onArrival, Vector3? targetFacing = null) { EnsureInit(); EnsureIndicator(); float clampedX = Mathf.Clamp(worldPos.x, minX, maxX); float clampedZ = Mathf.Clamp(worldPos.z, minZ, maxZ); // 避开中央喷泉碰撞体中心 Vector2 fountainCenter = new Vector2(0f, 0.8f); Vector2 target2D = new Vector2(clampedX, clampedZ); if (Vector2.Distance(target2D, fountainCenter) < 2.3f) { Vector2 dir = (target2D - fountainCenter).normalized; if (dir.sqrMagnitude < 0.001f) dir = Vector2.down; target2D = fountainCenter + dir * 2.35f; clampedX = target2D.x; clampedZ = target2D.y; } _targetPosition = new Vector3(clampedX, transform.position.y, clampedZ); _onArrival = onArrival; _targetFacing = targetFacing; _isMoving = true; if (_animator != null) { _animator.SetBool(_animParamIsMoving, true); _animator.SetFloat(_animParamSpeed, 1.0f); } if (_indicator != null) { _indicator.transform.position = _targetPosition; _indicator.SetActive(true); } } public void Stop() { EnsureInit(); _isMoving = false; _targetPosition = transform.position; if (_animator != null) { _animator.SetBool(_animParamIsMoving, false); _animator.SetFloat(_animParamSpeed, 0f); } if (_targetFacing.HasValue && _targetFacing.Value.sqrMagnitude > 0.001f) { transform.rotation = Quaternion.LookRotation(_targetFacing.Value.normalized, Vector3.up); } if (_indicator != null) { _indicator.SetActive(false); } var callback = _onArrival; _onArrival = null; _targetFacing = null; callback?.Invoke(); } void Update() { if (!_isMoving) return; Vector3 toTarget = _targetPosition - transform.position; toTarget.y = 0; float dist = toTarget.magnitude; if (dist <= stopDistance) { Stop(); return; } // 平滑转向目标 if (toTarget.sqrMagnitude > 0.0001f) { Quaternion targetRot = Quaternion.LookRotation(toTarget.normalized, Vector3.up); transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRot, turnSpeed * Time.deltaTime); } // 平滑移动前进 float step = Mathf.Min(moveSpeed * Time.deltaTime, dist); transform.position += toTarget.normalized * step; // 光圈轻微脉冲缩放 if (_indicator != null && _indicator.activeSelf) { float pulse = 1.0f + 0.12f * Mathf.Sin(Time.time * 6.5f); _indicator.transform.localScale = new Vector3(pulse, 1.0f, pulse); } } void OnDestroy() { if (_indicator != null) { Destroy(_indicator); } } } }