Files

230 lines
7.6 KiB
C#

using System;
using UnityEngine;
namespace HotUpdate.Gameplay
{
/// <summary>
/// 3D 营地主角探险家控制器:响应点击路面/建筑、平滑移动至目标位置并驱动待机与行走动画,支持到达事件与地面指示光圈。
/// </summary>
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<Animator>();
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<MeshFilter>();
var mr = _indicator.AddComponent<MeshRenderer>();
mf.sharedMesh = CreateRingMesh(0.85f, 1.25f, 32);
var smr = GetComponentInChildren<SkinnedMeshRenderer>();
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;
}
/// <summary>
/// 指挥探险家移动到指定世界坐标(单参数重载,保持反射与旧接口兼容)。
/// </summary>
public void MoveTo(Vector3 worldPos) => MoveTo(worldPos, null, null);
/// <summary>
/// 指挥探险家移动到指定世界坐标(自动对齐路面并做边界 Clamp)。
/// 可选指定到达回调与到达时的最终朝向。
/// </summary>
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);
}
}
}
}