From 053a95b4fb15955efee7a8a5276826fde8df75ff Mon Sep 17 00:00:00 2001 From: ud18010 Date: Tue, 28 Jul 2026 17:23:11 +0800 Subject: [PATCH] fix: polish lobby chat and joystick --- .../Art/UI/Prefab/UI_LobbyJoystick.prefab | 2 +- .../Tests/EditMode/WorldChatModuleTests.cs | 88 +++++++++++++++++++ .../EditMode/WorldChatModuleTests.cs.meta | 11 +++ .../EditMode/XWorld.Link.EditModeTests.asmdef | 1 + .../Script/xmain/Client/CSharpClientApp.cs | 17 +++- .../xmain/Client/LobbyWorldController.cs | 2 +- Client/Assets/Script/xmain/Module/Chat.meta | 8 ++ .../Module/{ => Chat}/WorldChatModule.cs | 42 +++++++++ .../Module/{ => Chat}/WorldChatModule.cs.meta | 6 +- Doc/UIPrefabCreater/UI_LobbyJoystick.json | 2 +- Server/Gateway/ServerLoop.cs | 6 +- 11 files changed, 176 insertions(+), 9 deletions(-) create mode 100644 Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs create mode 100644 Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs.meta create mode 100644 Client/Assets/Script/xmain/Module/Chat.meta rename Client/Assets/Script/xmain/Module/{ => Chat}/WorldChatModule.cs (88%) rename Client/Assets/Script/xmain/Module/{ => Chat}/WorldChatModule.cs.meta (76%) diff --git a/Client/Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab b/Client/Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab index 0b281dfb..d01deaeb 100644 --- a/Client/Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab +++ b/Client/Assets/Game/Art/UI/Prefab/UI_LobbyJoystick.prefab @@ -36,7 +36,7 @@ RectTransform: m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} m_AnchorMax: {x: 0, y: 0} - m_AnchoredPosition: {x: 208, y: 158} + m_AnchoredPosition: {x: 260, y: 220} m_SizeDelta: {x: 148, y: 148} m_Pivot: {x: 0.5, y: 0.5} --- !u!225 &8720693099983328153 diff --git a/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs b/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs new file mode 100644 index 00000000..aa1529fe --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Reflection; +using NUnit.Framework; +using TMPro; +using UnityEditor; +using UnityEngine; +using XGame; +using XWorld.Framework.Protocol; + +public sealed class WorldChatModuleTests +{ + private GameObject view; + private GameObject host; + + [TearDown] + public void TearDown() + { + if (view != null) + { + UnityEngine.Object.DestroyImmediate(view); + } + if (host != null) + { + UnityEngine.Object.DestroyImmediate(host); + } + } + + [Test] + public void EndEditingInput_SubmitsTrimmedWorldChat() + { + WorldChatModule module = CreateBoundModule(); + TMP_InputField input = view.transform.FindTransform("InputHitArea").GetComponent(); + string sent = null; + module.OnSendWorldChat = text => sent = text; + + input.text = " hello world "; + input.onEndEdit.Invoke(input.text); + + Assert.That(sent, Is.EqualTo("hello world")); + Assert.That(input.text, Is.Empty); + } + + [Test] + public void ReceivedMessage_UpdatesCollapsedPreview() + { + WorldChatModule module = CreateBoundModule(); + TMP_Text collapsedText = view.transform.FindTransform("Txt_CollapsedPlaceholder").GetComponent(); + + module.HandleWorldChatMessage(new WorldChatMessageMsg + { + PlayerId = 2, + PlayerName = "Bob", + Text = "hello world" + }); + + Assert.That(collapsedText.text, Is.EqualTo("Bob: hello world")); + } + + private WorldChatModule CreateBoundModule() + { + GameObject prefab = AssetDatabase.LoadAssetAtPath("Assets/Game/Art/UI/Prefab/UI_CommonChat.prefab"); + Assert.That(prefab, Is.Not.Null); + + view = UnityEngine.Object.Instantiate(prefab); + host = new GameObject("WorldChatModuleTestHost"); + WorldChatModule module = host.AddComponent(); + module.Initialize(1, _ => { }); + + SetPrivateField(module, "view", view); + InvokePrivate(module, "CacheControls"); + InvokePrivate(module, "BindEvents"); + return module; + } + + private static void SetPrivateField(object target, string name, object value) + { + FieldInfo field = target.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(field, Is.Not.Null); + field.SetValue(target, value); + } + + private static void InvokePrivate(object target, string name) + { + MethodInfo method = target.GetType().GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(method, Is.Not.Null); + method.Invoke(target, Array.Empty()); + } +} diff --git a/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs.meta b/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs.meta new file mode 100644 index 00000000..2096e83f --- /dev/null +++ b/Client/Assets/Script/Tests/EditMode/WorldChatModuleTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f953f6437b2d4314b8c8c77d531223e0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef b/Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef index dcf8a448..8a8c282f 100644 --- a/Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef +++ b/Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "", "references": [ "XWorld.Link", + "XWorld.Framework.Shared", "Unity.TextMeshPro", "GUID:119d45324677fde49923efc4ea54b313" ], diff --git a/Client/Assets/Script/xmain/Client/CSharpClientApp.cs b/Client/Assets/Script/xmain/Client/CSharpClientApp.cs index fa83ae3d..253f24d3 100644 --- a/Client/Assets/Script/xmain/Client/CSharpClientApp.cs +++ b/Client/Assets/Script/xmain/Client/CSharpClientApp.cs @@ -584,6 +584,7 @@ namespace XGame lobbyWorld?.HandleFrame(frame); break; case FrameworkOpcode.WorldChatMessage: + Debug.Log("[CSharpClientApp] recv WorldChatMessage bytes=" + (frame.Payload?.Length ?? 0)); worldChat?.HandleWorldChatMessage(WorldChatMessageMsg.Decode(frame.Payload)); break; } @@ -701,13 +702,25 @@ namespace XGame private void SendWorldChat(string text) { text = (text ?? string.Empty).Trim(); - if (text.Length == 0 || lobbyNet == null) + if (text.Length == 0) { return; } + if (lobbyNet == null) + { + Debug.LogWarning("[CSharpClientApp] drop WorldChatSend: lobbyNet is null"); + return; + } + if (!lobbyNet.IsConnected) + { + Debug.LogWarning("[CSharpClientApp] drop WorldChatSend: lobbyNet is disconnected"); + return; + } + byte[] payload = new WorldChatSendMsg { Text = text }.Encode(); + Debug.Log("[CSharpClientApp] send WorldChatSend bytes=" + payload.Length); lobbyNet.SendFramework( FrameworkOpcode.WorldChatSend, - new WorldChatSendMsg { Text = text }.Encode()); + payload); } private void CloseWorldChat() diff --git a/Client/Assets/Script/xmain/Client/LobbyWorldController.cs b/Client/Assets/Script/xmain/Client/LobbyWorldController.cs index 3d6bb95c..be57aea8 100644 --- a/Client/Assets/Script/xmain/Client/LobbyWorldController.cs +++ b/Client/Assets/Script/xmain/Client/LobbyWorldController.cs @@ -40,7 +40,7 @@ namespace XGame private const float DirectionChangeDot = 0.98f; private const float JoystickRadius = 74f; private const float JoystickKnobRadius = 28f; - private const float JoystickPadding = 34f; + private const float JoystickPadding = 100.0f; private const float JoystickDeadZone = 0.18f; private const float JoystickAcquireRadiusMultiplier = 1.6f; private const float CameraMinDistance = 3.5f; diff --git a/Client/Assets/Script/xmain/Module/Chat.meta b/Client/Assets/Script/xmain/Module/Chat.meta new file mode 100644 index 00000000..9feefba9 --- /dev/null +++ b/Client/Assets/Script/xmain/Module/Chat.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6508ec0f8203b6d4e868b5a74194a926 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Client/Assets/Script/xmain/Module/WorldChatModule.cs b/Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs similarity index 88% rename from Client/Assets/Script/xmain/Module/WorldChatModule.cs rename to Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs index 958cfe7d..876f0f14 100644 --- a/Client/Assets/Script/xmain/Module/WorldChatModule.cs +++ b/Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs @@ -24,10 +24,13 @@ namespace XGame private TMP_InputField inputField; private TMP_Text inputPlaceholder; private TMP_Text inputValue; + private TMP_Text collapsedPreviewText; private ScrollRect scrollMessages; private bool opening; private int openVersion; private int localPlayerId; + private string collapsedDefaultText; + private string lastCollapsedPreview; private readonly Queue messageRows = new Queue(); public Action OnSendWorldChat; @@ -89,7 +92,10 @@ namespace XGame inputField = null; inputPlaceholder = null; inputValue = null; + collapsedPreviewText = null; scrollMessages = null; + collapsedDefaultText = null; + lastCollapsedPreview = null; opening = false; } @@ -128,6 +134,12 @@ namespace XGame scrollMessages = FindComponent("Scroll_Messages"); inputPlaceholder = FindComponent("Txt_InputPlaceholder"); inputValue = FindComponent("Txt_InputValue"); + collapsedPreviewText = FindComponent("Txt_CollapsedPlaceholder"); + if (collapsedPreviewText != null && string.IsNullOrEmpty(collapsedDefaultText)) + { + collapsedDefaultText = collapsedPreviewText.text; + } + RefreshCollapsedPreview(); EnsureInputField(); } @@ -142,6 +154,8 @@ namespace XGame { inputField.onSubmit.RemoveListener(OnInputSubmitted); inputField.onSubmit.AddListener(OnInputSubmitted); + inputField.onEndEdit.RemoveListener(OnInputEndEdit); + inputField.onEndEdit.AddListener(OnInputEndEdit); } } @@ -259,6 +273,11 @@ namespace XGame SendCurrentInput(); } + private void OnInputEndEdit(string text) + { + SendCurrentInput(); + } + private void SendCurrentInput() { string text = inputField == null ? string.Empty : inputField.text; @@ -271,6 +290,7 @@ namespace XGame { text = text.Substring(0, MaxInputLength); } + Debug.Log("[WorldChatModule] send world chat length=" + text.Length); OnSendWorldChat?.Invoke(text); ClearInput(); FocusInput(); @@ -326,6 +346,28 @@ namespace XGame { StartCoroutine(ScrollToBottomNextFrame()); } + SetCollapsedPreview(message, isSelf); + } + + private void SetCollapsedPreview(WorldChatMessageMsg message, bool isSelf) + { + string name = isSelf ? "\u6211" : message.PlayerName; + if (string.IsNullOrWhiteSpace(name)) + { + name = "\u73A9\u5BB6"; + } + lastCollapsedPreview = name + ": " + (message.Text ?? string.Empty); + RefreshCollapsedPreview(); + } + + private void RefreshCollapsedPreview() + { + if (collapsedPreviewText != null) + { + collapsedPreviewText.text = string.IsNullOrEmpty(lastCollapsedPreview) + ? (collapsedDefaultText ?? string.Empty) + : lastCollapsedPreview; + } } private IEnumerator ScrollToBottomNextFrame() diff --git a/Client/Assets/Script/xmain/Module/WorldChatModule.cs.meta b/Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs.meta similarity index 76% rename from Client/Assets/Script/xmain/Module/WorldChatModule.cs.meta rename to Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs.meta index 6774ba8e..820343cb 100644 --- a/Client/Assets/Script/xmain/Module/WorldChatModule.cs.meta +++ b/Client/Assets/Script/xmain/Module/Chat/WorldChatModule.cs.meta @@ -6,6 +6,6 @@ MonoImporter: defaultReferences: [] executionOrder: 0 icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: + userData: + assetBundleName: + assetBundleVariant: diff --git a/Doc/UIPrefabCreater/UI_LobbyJoystick.json b/Doc/UIPrefabCreater/UI_LobbyJoystick.json index db0e2fea..ab9fcd4f 100644 --- a/Doc/UIPrefabCreater/UI_LobbyJoystick.json +++ b/Doc/UIPrefabCreater/UI_LobbyJoystick.json @@ -25,7 +25,7 @@ "anchorMin": [0, 0], "anchorMax": [0, 0], "pivot": [0.5, 0.5], - "anchoredPosition": [208, 158], + "anchoredPosition": [260, 220], "sizeDelta": [148, 148] }, "components": [ diff --git a/Server/Gateway/ServerLoop.cs b/Server/Gateway/ServerLoop.cs index 3a1d317a..5092e324 100644 --- a/Server/Gateway/ServerLoop.cs +++ b/Server/Gateway/ServerLoop.cs @@ -221,6 +221,7 @@ namespace XWorld.Server.Gateway { if (!_sessions.IsConnected(pid)) { + _logger.Warn($"world chat ignored pid={pid} reason=not_connected"); return; } @@ -228,6 +229,7 @@ namespace XWorld.Server.Gateway string text = (req.Text ?? string.Empty).Trim(); if (text.Length == 0) { + _logger.Warn($"world chat ignored pid={pid} reason=empty"); return; } if (text.Length > MaxWorldChatTextLength) @@ -246,7 +248,9 @@ namespace XWorld.Server.Gateway ServerTimeMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; byte[] encoded = msg.Encode(); - foreach (int targetPid in _sessions.ConnectedPlayerIds()) + IReadOnlyList targets = _sessions.ConnectedPlayerIds(); + _logger.Info($"world chat pid={pid} name={name} length={text.Length} targets={targets.Count}"); + foreach (int targetPid in targets) { SendFramework(targetPid, FrameworkOpcode.WorldChatMessage, encoded); }