Land Explorer in HomeCamp with boot loading, journal UI, and CDN publish tooling.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user