230 lines
7.6 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|