Files
AIC-Project/Client/Assets/Script/xmain/Client/LobbyWorldController.cs
T
2026-07-10 10:24:29 +08:00

1222 lines
46 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.UI;
using XWorld.Framework.Protocol;
using UObject = UnityEngine.Object;
namespace XGame
{
public sealed class LobbyWorldController : MonoBehaviour
{
private sealed class Avatar
{
public int PlayerId;
public int Figure;
public GameObject Obj;
public Animator Animator;
public Vector3 Target;
public bool HasMoveTarget;
public bool Moving;
public string AnimState;
public Vector3 LastFaceDirection = Vector3.forward;
public Vector3 StopFaceDirection;
public bool SuppressMoveFacing;
public bool HasServerPosition;
public bool IsPlaceholder; // Obj 当前是胶囊占位(真实角色预设还在异步加载),加载完成后会原地升级
}
private static readonly string[] ActorPaths =
{
"Game/Art/Actor/Prefab/Assasin.prefab",
"Game/Art/Actor/Prefab/bls.prefab",
"Game/Art/Actor/Prefab/boy.prefab",
"Game/Art/Actor/Prefab/captain.prefab",
"Game/Art/Actor/Prefab/habit.prefab",
"Game/Art/Actor/Prefab/rhino.prefab",
"Game/Art/Actor/Prefab/tiger.prefab",
};
private const string JoystickPrefabPath = "Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab";
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
private const float MoveSpeed = 1f;
private const float StopDistance = 0.05f;
private const float MoveSyncInterval = 1f;
private const float MovePredictionSeconds = 1.5f;
private const float DirectionChangeMinInterval = 0.1f;
private const float DirectionChangeDot = 0.98f;
private const float JoystickRadius = 74f;
private const float JoystickKnobRadius = 28f;
private const float JoystickPadding = 34f;
private const float JoystickDeadZone = 0.18f;
private const float JoystickAcquireRadiusMultiplier = 1.6f;
private const float CameraMinDistance = 3.5f;
private const float CameraMaxDistance = 14f;
private const float CameraDefaultDistance = 9.5f;
private const float CameraMinPitch = 20f;
private const float CameraMaxPitch = 75f;
private const float CameraMouseOrbitSensitivity = 0.2f;
private const float CameraTouchOrbitSensitivity = 0.12f;
private const float CameraMouseZoomSensitivity = 1.4f;
private const float CameraPinchZoomSensitivity = 0.015f;
private const float CameraFollowSharpness = 12f;
private readonly Dictionary<int, Avatar> avatars = new Dictionary<int, Avatar>();
private readonly Dictionary<int, GameObject> prefabCache = new Dictionary<int, GameObject>();
private readonly HashSet<int> prefabLoading = new HashSet<int>();
private Action<FrameworkOpcode, byte[]> send;
private Transform worldRoot;
private int selfPlayerId;
private int selfFigure;
private string selfName;
private bool joined;
private Vector2 joystick;
private bool joystickActive;
private float nextMoveCommandTime;
private float lastMoveCommandTime;
private Vector3 lastMoveDirection;
private bool receivedSelfState;
private float nextLocalMoveLogTime;
private float nextRemoteMoveLogTime;
private float cameraYaw;
private float cameraPitch = 43f;
private float cameraDistance = CameraDefaultDistance;
private bool cameraMouseDragging;
private bool cameraTouchDragging;
private int cameraTouchFingerId = -1;
private Vector2 lastCameraPointerPosition;
private bool cameraPinching;
private float lastPinchDistance;
private RectTransform joystickRoot;
private RectTransform joystickWidget; // 视觉容器:新预设里的 "Joystick" 子节点(自带定位);找不到则回退为 joystickRoot
private RectTransform joystickKnob;
private bool joystickMouseCaptured;
private int joystickTouchFingerId = -1;
private bool joystickLoading;
private static int materialLogCount;
public void Initialize(Action<FrameworkOpcode, byte[]> sendFramework, int playerId, string playerName, int figure)
{
send = sendFramework;
selfPlayerId = playerId;
selfName = string.IsNullOrEmpty(playerName) ? "P" + playerId : playerName;
selfFigure = Mathf.Clamp(figure <= 0 ? GetSelectedFigure() : figure, 1, ActorPaths.Length);
EnsureWorldRoot();
worldRoot.gameObject.SetActive(true);
EnsureCamera();
StartCoroutine(EnsureJoystickUIAsync());
GetOrCreateAvatar(selfPlayerId, selfFigure, selfName);
receivedSelfState = false;
lastMoveDirection = Vector3.zero;
nextMoveCommandTime = 0f;
lastMoveCommandTime = -DirectionChangeMinInterval;
Debug.Log("[LobbyWorldController] initialize self pid=" + selfPlayerId + " name=" + selfName + " figure=" + selfFigure);
SendJoin();
}
public void SendJoin()
{
if (send == null || selfPlayerId <= 0)
{
return;
}
joined = true;
if (joystickRoot != null)
{
joystickRoot.gameObject.SetActive(true);
}
send(FrameworkOpcode.LobbyJoin, new LobbyJoinMsg { Name = selfName, Figure = selfFigure }.Encode());
}
public void SendLeave()
{
if (!joined || send == null)
{
return;
}
joined = false;
send(FrameworkOpcode.LobbyLeave, Array.Empty<byte>());
if (worldRoot != null)
{
worldRoot.gameObject.SetActive(false);
}
if (joystickRoot != null)
{
joystickRoot.gameObject.SetActive(false);
}
}
public void RefreshSelectedFigure()
{
int nextFigure = Mathf.Clamp(GetSelectedFigure(), 1, ActorPaths.Length);
if (selfFigure == nextFigure)
{
return;
}
selfFigure = nextFigure;
Vector3 position = Vector3.zero;
Quaternion rotation = Quaternion.identity;
bool hadAvatar = false;
if (avatars.TryGetValue(selfPlayerId, out Avatar avatar))
{
if (avatar.Obj != null)
{
position = avatar.Obj.transform.position;
rotation = avatar.Obj.transform.rotation;
hadAvatar = true;
}
Destroy(avatar.Obj);
avatars.Remove(selfPlayerId);
}
Avatar nextAvatar = GetOrCreateAvatar(selfPlayerId, selfFigure, selfName);
if (hadAvatar && nextAvatar.Obj != null)
{
nextAvatar.Obj.transform.position = position;
nextAvatar.Obj.transform.rotation = rotation;
nextAvatar.Target = position;
}
lastMoveDirection = Vector3.zero;
nextMoveCommandTime = 0f;
lastMoveCommandTime = -DirectionChangeMinInterval;
SendJoin();
}
public void HandleFrame(Frame frame)
{
if (frame.Opcode == (ushort)FrameworkOpcode.LobbyState)
{
ApplyState(LobbyStateMsg.Decode(frame.Payload));
}
else if (frame.Opcode == (ushort)FrameworkOpcode.LobbyMove)
{
ApplyMove(LobbyMoveMsg.Decode(frame.Payload));
}
}
private void Update()
{
UpdateRemoteAvatars();
UpdateLocalMove();
UpdateCamera();
}
private void ApplyState(LobbyStateMsg state)
{
ReconcileSelfPlayerId(state);
Debug.Log("[LobbyWorldController] state players=" + DescribeStatePlayers(state) + " self=" + selfPlayerId);
var seen = new HashSet<int>();
foreach (LobbyPlayerMsg player in state.Players)
{
seen.Add(player.PlayerId);
Avatar avatar = GetOrCreateAvatar(player.PlayerId, player.Figure, player.Name);
Vector3 pos = new Vector3(player.X, player.Y, player.Z);
if (player.PlayerId == selfPlayerId)
{
if (!receivedSelfState)
{
avatar.Obj.transform.position = pos;
avatar.Target = pos;
avatar.HasServerPosition = true;
}
receivedSelfState = true;
continue;
}
if (!avatar.HasServerPosition && avatar.Obj != null)
{
avatar.Obj.transform.position = pos;
avatar.HasServerPosition = true;
}
avatar.Target = pos;
avatar.Moving = player.Moving;
}
var remove = new List<int>();
foreach (int id in avatars.Keys)
{
if (!seen.Contains(id))
{
remove.Add(id);
}
}
foreach (int id in remove)
{
Destroy(avatars[id].Obj);
avatars.Remove(id);
}
}
private static string DescribeStatePlayers(LobbyStateMsg state)
{
if (state == null || state.Players == null || state.Players.Count == 0)
{
return "[]";
}
string result = "[";
for (int i = 0; i < state.Players.Count; i++)
{
LobbyPlayerMsg player = state.Players[i];
if (i > 0)
{
result += ", ";
}
result += player.PlayerId + ":" + player.Name + "@" + player.X.ToString("0.00") + "," + player.Z.ToString("0.00");
}
return result + "]";
}
private void ReconcileSelfPlayerId(LobbyStateMsg state)
{
if (state == null || state.Players == null || state.Players.Count == 0)
{
return;
}
for (int i = 0; i < state.Players.Count; i++)
{
if (state.Players[i].PlayerId == selfPlayerId)
{
return;
}
}
int serverPlayerId = 0;
if (!string.IsNullOrEmpty(selfName))
{
for (int i = 0; i < state.Players.Count; i++)
{
if (state.Players[i].Name == selfName)
{
serverPlayerId = state.Players[i].PlayerId;
break;
}
}
}
if (serverPlayerId <= 0 && state.Players.Count == 1)
{
serverPlayerId = state.Players[0].PlayerId;
}
if (serverPlayerId <= 0 || serverPlayerId == selfPlayerId)
{
return;
}
int oldPlayerId = selfPlayerId;
if (avatars.TryGetValue(oldPlayerId, out Avatar oldAvatar))
{
avatars.Remove(oldPlayerId);
if (!avatars.ContainsKey(serverPlayerId))
{
oldAvatar.PlayerId = serverPlayerId;
avatars[serverPlayerId] = oldAvatar;
}
else if (oldAvatar.Obj != null)
{
Destroy(oldAvatar.Obj);
}
}
selfPlayerId = serverPlayerId;
Debug.Log("[LobbyWorldController] reconcile self player id " + oldPlayerId + " -> " + serverPlayerId);
}
private void ApplyMove(LobbyMoveMsg move)
{
if (!avatars.TryGetValue(move.PlayerId, out Avatar avatar))
{
avatar = GetOrCreateAvatar(move.PlayerId, 1, "P" + move.PlayerId);
}
Vector3 target = new Vector3(move.X, move.Y, move.Z);
if (!avatar.HasServerPosition && avatar.Obj != null)
{
avatar.Obj.transform.position = target;
avatar.HasServerPosition = true;
}
avatar.Target = target;
avatar.Moving = move.Moving;
Vector3 face = new Vector3(move.DirX, 0f, move.DirZ);
if (move.PlayerId == selfPlayerId)
{
return;
}
if (Time.unscaledTime >= nextRemoteMoveLogTime)
{
nextRemoteMoveLogTime = Time.unscaledTime + 1f;
Debug.Log("[LobbyWorldController] remote move pid=" + move.PlayerId + " target=" + move.X.ToString("0.00") + "," + move.Z.ToString("0.00") + " moving=" + move.Moving);
}
if (face.sqrMagnitude > 0.001f)
{
FaceAvatar(avatar, face);
}
}
private void UpdateLocalMove()
{
if (!joined || !avatars.TryGetValue(selfPlayerId, out Avatar avatar) || avatar.Obj == null)
{
return;
}
UpdateJoystick();
Vector3 dir = GetMoveDirection();
bool moving = dir.sqrMagnitude > 0.001f;
if (moving)
{
dir.Normalize();
MoveLocalAvatar(avatar, dir);
bool directionChanged = DirectionChanged(dir, lastMoveDirection);
bool canSendDirectionChange = Time.unscaledTime >= lastMoveCommandTime + DirectionChangeMinInterval;
if (Time.unscaledTime >= nextMoveCommandTime || (directionChanged && canSendDirectionChange))
{
Vector3 target = avatar.Obj.transform.position + dir * MoveSpeed * MovePredictionSeconds;
SendMove(target, dir, true);
lastMoveDirection = dir;
nextMoveCommandTime = Time.unscaledTime + MoveSyncInterval;
}
}
else if (lastMoveDirection.sqrMagnitude > 0.001f)
{
avatar.StopFaceDirection = lastMoveDirection;
avatar.SuppressMoveFacing = true;
avatar.Target = avatar.Obj.transform.position;
avatar.HasMoveTarget = false;
avatar.Moving = false;
SetMoveState(avatar, false);
SendMove(avatar.Obj.transform.position, lastMoveDirection, false);
lastMoveDirection = Vector3.zero;
nextMoveCommandTime = Time.unscaledTime;
}
}
private Vector3 GetMoveDirection()
{
Camera cam = Camera.main;
Vector3 forward = cam != null ? cam.transform.forward : Vector3.forward;
Vector3 right = cam != null ? cam.transform.right : Vector3.right;
forward.y = 0f;
right.y = 0f;
forward = forward.sqrMagnitude > 0.001f ? forward.normalized : Vector3.forward;
right = right.sqrMagnitude > 0.001f ? right.normalized : Vector3.right;
Vector3 dir = Vector3.zero;
if (Input.GetKey(KeyCode.W)) dir += forward;
if (Input.GetKey(KeyCode.S)) dir -= forward;
if (Input.GetKey(KeyCode.A)) dir -= right;
if (Input.GetKey(KeyCode.D)) dir += right;
if (joystickActive) dir += right * joystick.x + forward * joystick.y;
return dir;
}
private void UpdateRemoteAvatars()
{
foreach (Avatar avatar in avatars.Values)
{
if (avatar.PlayerId == selfPlayerId || avatar.Obj == null)
{
continue;
}
avatar.Obj.transform.position = Vector3.MoveTowards(avatar.Obj.transform.position, avatar.Target, MoveSpeed * Time.deltaTime);
SetMoveState(avatar, avatar.Moving);
if (avatar.Moving && Vector3.Distance(avatar.Obj.transform.position, avatar.Target) <= StopDistance)
{
avatar.Moving = false;
SetMoveState(avatar, false);
}
}
}
private void ApplyLocalMoveTo(Avatar avatar, Vector3 target, Vector3 direction)
{
avatar.Target = target;
avatar.HasMoveTarget = true;
avatar.Moving = true;
avatar.SuppressMoveFacing = false;
FaceAvatar(avatar, direction);
SetMoveState(avatar, true);
}
private void MoveLocalAvatar(Avatar avatar, Vector3 direction)
{
Vector3 pos = avatar.Obj.transform.position;
pos += direction * MoveSpeed * Time.deltaTime;
avatar.Obj.transform.position = pos;
avatar.Target = pos;
avatar.HasMoveTarget = true;
avatar.Moving = true;
avatar.SuppressMoveFacing = false;
FaceAvatar(avatar, direction);
SetMoveState(avatar, true);
}
private void AdvanceLocalAvatar(Avatar avatar)
{
if (!avatar.HasMoveTarget)
{
return;
}
Vector3 offset = avatar.Target - avatar.Obj.transform.position;
offset.y = 0f;
if (offset.magnitude <= StopDistance)
{
avatar.Obj.transform.position = avatar.Target;
avatar.HasMoveTarget = false;
avatar.Moving = false;
if (avatar.StopFaceDirection.sqrMagnitude > 0.001f)
{
FaceAvatar(avatar, avatar.StopFaceDirection);
}
avatar.StopFaceDirection = Vector3.zero;
avatar.SuppressMoveFacing = false;
SetMoveState(avatar, false);
return;
}
Vector3 stepDirection = offset.normalized;
avatar.Obj.transform.position = Vector3.MoveTowards(avatar.Obj.transform.position, avatar.Target, MoveSpeed * Time.deltaTime);
if (!avatar.SuppressMoveFacing)
{
FaceAvatar(avatar, stepDirection);
}
SetMoveState(avatar, true);
}
private void SendMove(Vector3 position, Vector3 direction, bool moving)
{
if (send == null)
{
return;
}
if (Time.unscaledTime >= nextLocalMoveLogTime)
{
nextLocalMoveLogTime = Time.unscaledTime + 1f;
Debug.Log("[LobbyWorldController] send move self=" + selfPlayerId + " target=" + position.x.ToString("0.00") + "," + position.z.ToString("0.00") + " moving=" + moving);
}
send(FrameworkOpcode.LobbyMove, new LobbyMoveMsg
{
PlayerId = selfPlayerId,
X = position.x,
Y = position.y,
Z = position.z,
DirX = direction.x,
DirZ = direction.z,
Moving = moving,
}.Encode());
lastMoveCommandTime = Time.unscaledTime;
}
private Avatar GetOrCreateAvatar(int playerId, int figure, string name)
{
figure = Mathf.Clamp(figure <= 0 ? 1 : figure, 1, ActorPaths.Length);
if (avatars.TryGetValue(playerId, out Avatar avatar))
{
if (avatar.Figure == figure)
{
return avatar;
}
Destroy(avatar.Obj);
avatars.Remove(playerId);
}
avatar = new Avatar { PlayerId = playerId, Figure = figure };
avatar.Obj = InstantiateAvatar(figure, out bool isPlaceholder);
avatar.IsPlaceholder = isPlaceholder;
avatar.Obj.name = "LobbyPlayer_" + playerId + "_" + name;
avatar.Target = avatar.Obj.transform.position;
avatar.Animator = avatar.Obj.GetComponentInChildren<Animator>();
avatars[playerId] = avatar;
SetMoveState(avatar, false);
return avatar;
}
private static bool DirectionChanged(Vector3 lhs, Vector3 rhs)
{
if (rhs.sqrMagnitude <= 0.001f)
{
return true;
}
lhs.y = 0f;
rhs.y = 0f;
lhs.Normalize();
rhs.Normalize();
return Vector3.Dot(lhs, rhs) < DirectionChangeDot;
}
private static void FaceAvatar(Avatar avatar, Vector3 direction)
{
if (avatar == null || avatar.Obj == null)
{
return;
}
direction.y = 0f;
if (direction.sqrMagnitude <= 0.001f)
{
return;
}
Vector3 forward = direction.normalized;
avatar.Obj.transform.forward = forward;
avatar.LastFaceDirection = forward;
}
private GameObject InstantiateAvatar(int figure, out bool isPlaceholder)
{
GameObject prefab = GetCachedPrefabOrLoadAsync(figure);
isPlaceholder = prefab == null;
GameObject obj = prefab != null ? Instantiate(prefab, worldRoot) : GameObject.CreatePrimitive(PrimitiveType.Capsule);
if (prefab == null)
{
obj.transform.SetParent(worldRoot, false);
obj.transform.localScale = new Vector3(0.7f, 1f, 0.7f);
}
obj.SetActive(true);
ConfigureAvatarLightingAndLog(obj, "instantiate figure=" + figure);
return obj;
}
// 命中缓存则同步返回真实预设;未命中则返回 null(调用方用胶囊占位),并异步从 CDN 加载,
// 完成后写入缓存并把所有该 figure 的占位胶囊原地升级为真实角色。统一异步、避免读到旧底包。
private GameObject GetCachedPrefabOrLoadAsync(int figure)
{
if (prefabCache.TryGetValue(figure, out GameObject prefab) && prefab != null)
{
return prefab;
}
if (prefabLoading.Add(figure))
{
StartCoroutine(CoLoadActorPrefab(figure));
}
return null;
}
private IEnumerator CoLoadActorPrefab(int figure)
{
UObject loaded = null;
yield return XResLoader.coLoadRes(ActorPaths[figure - 1], typeof(GameObject), o => loaded = o);
prefabLoading.Remove(figure);
GameObject prefab = loaded as GameObject;
if (prefab == null)
{
Debug.LogWarning("[LobbyWorldController] load actor prefab failed figure=" + figure);
yield break;
}
prefabCache[figure] = prefab;
UpgradePlaceholders(figure, prefab);
}
// 真实角色预设加载完成后,把当前还在用胶囊占位的同 figure 头像原地替换为真实角色,保留位置/朝向/状态。
private void UpgradePlaceholders(int figure, GameObject prefab)
{
foreach (var kv in avatars)
{
Avatar a = kv.Value;
if (a == null || a.Figure != figure || !a.IsPlaceholder || a.Obj == null)
{
continue;
}
Vector3 pos = a.Obj.transform.position;
Quaternion rot = a.Obj.transform.rotation;
string objName = a.Obj.name;
Destroy(a.Obj);
a.Obj = Instantiate(prefab, worldRoot);
a.Obj.name = objName;
a.Obj.transform.position = pos;
a.Obj.transform.rotation = rot;
a.Obj.SetActive(true);
ConfigureAvatarLightingAndLog(a.Obj, "upgrade figure=" + figure);
a.Animator = a.Obj.GetComponentInChildren<Animator>();
a.IsPlaceholder = false;
a.AnimState = null; // 让 SetMoveState 重新触发动画
SetMoveState(a, a.Moving);
}
}
private static void ConfigureAvatarLightingAndLog(GameObject obj, string reason)
{
if (obj == null)
{
return;
}
Renderer[] renderers = obj.GetComponentsInChildren<Renderer>(true);
foreach (Renderer renderer in renderers)
{
if (renderer == null)
{
continue;
}
renderer.lightProbeUsage = LightProbeUsage.BlendProbes;
renderer.reflectionProbeUsage = ReflectionProbeUsage.BlendProbes;
if (materialLogCount >= 24)
{
continue;
}
Material[] materials = renderer.sharedMaterials;
for (int i = 0; i < materials.Length && materialLogCount < 24; i++)
{
Material mat = materials[i];
if (mat == null)
{
continue;
}
Shader shader = mat.shader;
bool shaderRebound = false;
if (shader != null)
{
Shader runtimeShader = Game.GetRuntimeShader(shader.name);
if (runtimeShader != null && runtimeShader != shader)
{
mat.shader = runtimeShader;
shader = runtimeShader;
shaderRebound = true;
}
}
Texture mainTex = mat.HasProperty("_MainTex") ? mat.GetTexture("_MainTex") : null;
string textureInfo = mainTex != null ? mainTex.name : "null";
Texture2D tex2D = mainTex as Texture2D;
if (tex2D != null)
{
textureInfo += "/" + tex2D.format + "/" + tex2D.graphicsFormat;
}
Debug.Log("[LobbyWorldController] material " + reason +
" renderer=" + renderer.name +
" lightProbe=" + renderer.lightProbeUsage +
" reflectionProbe=" + renderer.reflectionProbeUsage +
" lightmapIndex=" + renderer.lightmapIndex +
" mat=" + mat.name +
" shader=" + (shader != null ? shader.name : "null") +
" supported=" + (shader != null && shader.isSupported) +
" shaderRebound=" + shaderRebound +
" keywords=" + string.Join(",", mat.shaderKeywords) +
" AO=" + (mat.HasProperty("_AO") ? mat.GetFloat("_AO").ToString("0.###") : "n/a") +
" UseMix=" + (mat.HasProperty("_UseMixTex") ? mat.GetFloat("_UseMixTex").ToString("0.###") : "n/a") +
" tex=" + textureInfo);
materialLogCount++;
}
}
}
private void SetMoveState(Avatar avatar, bool moving)
{
if (avatar.Animator == null)
{
return;
}
string state = moving ? "Walk" : "Stand";
if (avatar.AnimState == state)
{
return;
}
avatar.AnimState = state;
avatar.Animator.CrossFade(state, 0.1f);
}
private void UpdateJoystick()
{
joystick = Vector2.zero;
joystickActive = false;
Vector2 center = GetJoystickScreenCenter();
float radius = GetJoystickScreenRadius();
Vector2? pointer = null;
if (Input.touchCount > 0)
{
pointer = GetJoystickTouchPointer();
joystickMouseCaptured = false;
}
else
{
joystickTouchFingerId = -1;
if (Input.GetMouseButtonDown(0))
{
joystickMouseCaptured = IsInJoystickArea(Input.mousePosition);
}
if (Input.GetMouseButtonUp(0))
{
joystickMouseCaptured = false;
}
if (joystickMouseCaptured && Input.GetMouseButton(0))
{
// 一旦按住摇杆,拖到任意位置都继续跟随(方向取自中心、幅度钳到最大),松手才释放
pointer = Input.mousePosition;
}
}
if (pointer == null)
{
UpdateJoystickVisual();
return;
}
Vector2 raw = (pointer.Value - center) / radius;
joystick = Vector2.ClampMagnitude(raw, 1f);
joystickActive = joystick.magnitude >= JoystickDeadZone;
UpdateJoystickVisual();
}
private Vector2? GetJoystickTouchPointer()
{
if (joystickTouchFingerId >= 0)
{
for (int i = 0; i < Input.touchCount; i++)
{
Touch touch = Input.GetTouch(i);
if (touch.fingerId != joystickTouchFingerId)
{
continue;
}
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
joystickTouchFingerId = -1;
return null;
}
// 已按住:拖到任意位置都继续跟随,抬指(Ended/Canceled)才释放
return touch.position;
}
joystickTouchFingerId = -1;
}
for (int i = 0; i < Input.touchCount; i++)
{
Touch touch = Input.GetTouch(i);
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
continue;
}
if (IsInJoystickArea(touch.position))
{
joystickTouchFingerId = touch.fingerId;
return touch.position;
}
}
return null;
}
private void UpdateJoystickVisual()
{
if (joystickRoot == null || joystickKnob == null)
{
return;
}
joystickRoot.gameObject.SetActive(joined);
joystickKnob.anchoredPosition = joystick * GetKnobTravelRadius();
}
// 摇杆头在视觉容器内的最大行程(本地单位):容器半径 - 摇杆头半径,按预设实际尺寸自适应,避免出界
private float GetKnobTravelRadius()
{
RectTransform w = joystickWidget != null ? joystickWidget : joystickRoot;
if (w == null)
{
return JoystickRadius;
}
float widgetHalf = Mathf.Min(w.rect.width, w.rect.height) * 0.5f;
float knobHalf = joystickKnob != null ? Mathf.Min(joystickKnob.rect.width, joystickKnob.rect.height) * 0.5f : 0f;
return Mathf.Max(1f, widgetHalf - knobHalf);
}
private void EnsureWorldRoot()
{
if (worldRoot != null)
{
return;
}
GameObject root = GameObject.Find("LobbyWorld");
if (root == null)
{
root = new GameObject("LobbyWorld");
}
worldRoot = root.transform;
}
private void EnsureCamera()
{
if (Camera.main != null)
{
return;
}
GameObject camObj = new GameObject("LobbyCamera");
camObj.tag = "MainCamera";
camObj.AddComponent<Camera>();
camObj.transform.position = new Vector3(0f, 7f, -8f);
camObj.transform.rotation = Quaternion.Euler(55f, 0f, 0f);
}
private IEnumerator EnsureJoystickUIAsync()
{
EnsureEventSystem();
if (joystickRoot != null)
{
joystickRoot.gameObject.SetActive(joined);
yield break;
}
if (joystickLoading)
{
yield break;
}
joystickLoading = true;
GameObject rootObj = null;
UObject prefab = null;
yield return XResLoader.coLoadRes(JoystickPrefabPath, typeof(GameObject), o => prefab = o);
joystickLoading = false;
// 异步期间可能已被其它调用创建好,避免重复
if (joystickRoot != null)
{
joystickRoot.gameObject.SetActive(joined);
yield break;
}
// 摇杆是抬头 HUD → 挂到 UIRoot/HUD 层(见 UI 挂载规则);Main 未就绪时降级到本地 HUD 画布
Transform parent = GetHudParent();
GameObject prefabObj = prefab as GameObject;
if (prefabObj == null)
{
Debug.LogError("[LobbyWorldController] load joystick prefab failed: " + JoystickPrefabPath);
yield break;
}
rootObj = Instantiate(prefabObj, parent, false);
rootObj.name = "UI_LobbyJoystick";
joystickRoot = rootObj.GetComponent<RectTransform>();
if (joystickRoot == null)
{
joystickRoot = rootObj.AddComponent<RectTransform>();
}
joystickRoot.localScale = Vector3.one; // 兜底:设备包 prefab 根缩放异常时仍可见
// 视觉容器:新预设里是 "Joystick" 子节点(预设自带定位);找不到则用根并由 ApplyJoystickLayout 代码定位
Transform widget = joystickRoot.FindTransform("Joystick");
joystickWidget = (widget as RectTransform) ?? joystickRoot;
if (joystickWidget == joystickRoot)
{
ApplyJoystickLayout(joystickRoot);
}
// Knob 递归查找,兼容"直接子节点"(代码兜底版)与"Joystick 子节点"(新预设)两种结构
Transform knob = joystickRoot.FindTransform("Knob");
joystickKnob = knob as RectTransform;
if (joystickKnob == null)
{
Debug.LogError("[LobbyWorldController] joystick prefab missing Knob node: " + JoystickPrefabPath);
Destroy(rootObj);
joystickRoot = null;
joystickWidget = null;
yield break;
}
joystickRoot.gameObject.SetActive(joined);
UpdateJoystickVisual();
}
private Transform GetHudParent()
{
RectTransform hud = UIManager.GetLayer(UILayer.HUD);
if (hud != null)
{
return hud;
}
return EnsureHudCanvas().transform; // 降级:Main 未就绪时用本地创建的 HUD 画布
}
private Canvas EnsureHudCanvas()
{
EnsureEventSystem();
// 优先复用主 UI 画布(Main)——与其它大厅 UI 一致(AttachToMain)。运行时新建的 Overlay 画布
// 在本设备上世界缩放为 0(lossyScale=0)导致摇杆不可见;复用已验证可用的 Main 画布可避免该问题。
GameObject main = GameObject.Find("Main");
if (main != null)
{
Canvas mainCanvas = main.GetComponentInParent<Canvas>();
if (mainCanvas != null)
{
return mainCanvas;
}
}
GameObject canvasObj = GameObject.Find("LobbyHudCanvas");
Canvas canvas = canvasObj != null ? canvasObj.GetComponent<Canvas>() : null;
if (canvas != null)
{
return canvas;
}
canvasObj = new GameObject("LobbyHudCanvas", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasObj.GetComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 50;
CanvasScaler scaler = canvasObj.GetComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1280f, 720f);
scaler.matchWidthOrHeight = 0.5f;
return canvas;
}
private static void EnsureEventSystem()
{
if (EventSystem.current != null)
{
return;
}
GameObject eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
private static void ApplyJoystickLayout(RectTransform rect)
{
rect.localScale = Vector3.one; // 强制缩放为 1:设备包里的 prefab 根缩放可能是 0(会导致整体不可见)
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.zero;
rect.pivot = new Vector2(0.5f, 0.5f);
rect.anchoredPosition = new Vector2(JoystickPadding + JoystickRadius, JoystickPadding + JoystickRadius);
rect.sizeDelta = Vector2.one * (JoystickRadius * 2f);
}
private void UpdateCamera()
{
if (!avatars.TryGetValue(selfPlayerId, out Avatar avatar) || avatar.Obj == null)
{
return;
}
Camera cam = Camera.main;
if (cam == null)
{
return;
}
UpdateCameraInput();
Vector3 lookAt = avatar.Obj.transform.position + Vector3.up * 1.2f;
Quaternion orbit = Quaternion.Euler(cameraPitch, cameraYaw, 0f);
Vector3 targetPosition = lookAt + orbit * (Vector3.back * cameraDistance);
float t = 1f - Mathf.Exp(-CameraFollowSharpness * Time.deltaTime);
cam.transform.position = Vector3.Lerp(cam.transform.position, targetPosition, t);
cam.transform.LookAt(lookAt);
}
private void UpdateCameraInput()
{
UpdateCameraMouseInput();
UpdateCameraTouchInput();
cameraPitch = Mathf.Clamp(cameraPitch, CameraMinPitch, CameraMaxPitch);
cameraDistance = Mathf.Clamp(cameraDistance, CameraMinDistance, CameraMaxDistance);
}
private void UpdateCameraMouseInput()
{
bool pointerOverUi = IsPointerOverUi(-1);
float scroll = Input.mouseScrollDelta.y;
if (!pointerOverUi && Mathf.Abs(scroll) > 0.001f)
{
cameraDistance -= scroll * CameraMouseZoomSensitivity;
}
if (Input.touchCount > 0)
{
cameraMouseDragging = false;
return;
}
if (Input.GetMouseButtonDown(0))
{
Vector2 pos = Input.mousePosition;
cameraMouseDragging = !pointerOverUi && !IsInJoystickArea(pos);
lastCameraPointerPosition = pos;
}
else if (Input.GetMouseButtonUp(0))
{
cameraMouseDragging = false;
}
if (!cameraMouseDragging || !Input.GetMouseButton(0))
{
return;
}
Vector2 current = Input.mousePosition;
if (IsPointerOverUi(-1))
{
lastCameraPointerPosition = current;
return;
}
Vector2 delta = current - lastCameraPointerPosition;
lastCameraPointerPosition = current;
ApplyCameraOrbit(delta, CameraMouseOrbitSensitivity);
}
private void UpdateCameraTouchInput()
{
int cameraTouchCount = 0;
Touch first = default;
Touch second = default;
for (int i = 0; i < Input.touchCount; i++)
{
Touch touch = Input.GetTouch(i);
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
{
continue;
}
if (IsInJoystickArea(touch.position))
{
continue;
}
if (IsPointerOverUi(touch.fingerId))
{
continue;
}
if (cameraTouchCount == 0)
{
first = touch;
}
else if (cameraTouchCount == 1)
{
second = touch;
}
cameraTouchCount++;
}
if (cameraTouchCount >= 2)
{
cameraTouchDragging = false;
cameraTouchFingerId = -1;
float pinchDistance = Vector2.Distance(first.position, second.position);
if (cameraPinching)
{
cameraDistance -= (pinchDistance - lastPinchDistance) * CameraPinchZoomSensitivity;
}
cameraPinching = true;
lastPinchDistance = pinchDistance;
return;
}
cameraPinching = false;
if (cameraTouchCount == 0)
{
cameraTouchDragging = false;
cameraTouchFingerId = -1;
return;
}
Touch active = first;
if (active.phase == TouchPhase.Began)
{
if (IsPointerOverUi(active.fingerId))
{
cameraTouchDragging = false;
cameraTouchFingerId = -1;
return;
}
cameraTouchDragging = true;
cameraTouchFingerId = active.fingerId;
lastCameraPointerPosition = active.position;
return;
}
if (!cameraTouchDragging || active.fingerId != cameraTouchFingerId)
{
return;
}
Vector2 delta = active.position - lastCameraPointerPosition;
lastCameraPointerPosition = active.position;
ApplyCameraOrbit(delta, CameraTouchOrbitSensitivity);
}
private static bool IsPointerOverUi(int pointerId)
{
EventSystem eventSystem = EventSystem.current;
if (eventSystem == null)
{
return false;
}
return pointerId >= 0
? eventSystem.IsPointerOverGameObject(pointerId)
: eventSystem.IsPointerOverGameObject();
}
private void ApplyCameraOrbit(Vector2 delta, float sensitivity)
{
cameraYaw += delta.x * sensitivity;
cameraPitch -= delta.y * sensitivity;
}
// 摇杆所在画布的渲染相机:Overlay 用 nullScreenSpaceCamera/WorldSpace 用画布的 worldCamera。
// 不能一律传 null —— 相机空间画布的世界坐标(带缩放/平面距离)用 null 换算会得到错误屏幕点。
private Camera GetUICamera()
{
RectTransform w = joystickWidget != null ? joystickWidget : joystickRoot;
if (w == null)
{
return null;
}
Canvas cv = w.GetComponentInParent<Canvas>();
if (cv == null)
{
return null;
}
cv = cv.rootCanvas;
return cv.renderMode == RenderMode.ScreenSpaceOverlay ? null : cv.worldCamera;
}
private Vector2 GetJoystickScreenCenter()
{
RectTransform w = joystickWidget != null ? joystickWidget : joystickRoot;
if (w != null)
{
return RectTransformUtility.WorldToScreenPoint(GetUICamera(), w.position);
}
return new Vector2(JoystickPadding + JoystickRadius, JoystickPadding + JoystickRadius);
}
private float GetJoystickScreenRadius()
{
RectTransform w = joystickWidget != null ? joystickWidget : joystickRoot;
if (w == null)
{
return JoystickRadius;
}
Camera cam = GetUICamera();
Vector3[] corners = new Vector3[4];
w.GetWorldCorners(corners);
// 世界角点先转屏幕,再算屏幕空间半径(相机空间画布世界单位很小,不能直接用世界差值)
Vector2 s0 = RectTransformUtility.WorldToScreenPoint(cam, corners[0]);
Vector2 s2 = RectTransformUtility.WorldToScreenPoint(cam, corners[2]);
float width = Mathf.Abs(s2.x - s0.x);
float height = Mathf.Abs(s2.y - s0.y);
return Mathf.Max(1f, Mathf.Min(width, height) * 0.5f);
}
private bool IsInJoystickArea(Vector2 screenPosition)
{
Vector2 center = GetJoystickScreenCenter();
float radius = GetJoystickScreenRadius();
return Vector2.Distance(screenPosition, center) <= radius * JoystickAcquireRadiusMultiplier;
}
private static int GetSelectedFigure()
{
string selected = PlayerPrefs.GetString(SelectedCharacterKey, string.Empty).ToLowerInvariant();
for (int i = 0; i < ActorPaths.Length; i++)
{
string path = ActorPaths[i].ToLowerInvariant();
int slash = path.LastIndexOf('/');
int dot = path.LastIndexOf('.');
string id = dot > slash ? path.Substring(slash + 1, dot - slash - 1) : path;
if (id == selected)
{
return i + 1;
}
}
return 1;
}
}
}