This commit is contained in:
2026-10-06 01:14:13 +08:00
parent b05ebeeb90
commit 1f7b9d837f
13 changed files with 560 additions and 46 deletions
@@ -35,6 +35,8 @@ public class ConstEventString
public const string PlayerAfkNotice = "PlayerAfkNotice";
/// <summary>玩家取消挂机通知</summary>
public const string PlayerCancelAfkNotice = "PlayerCancelAfkNotice";
/// <summary>玩家信息同步(GetPlayerInfo 的新响应,含完整玩家数据)</summary>
public const string OnPlayerInfo = "OnPlayerInfo";
/// <summary>召唤卡召唤通知:某玩家使用召唤卡,服务端已创建召唤鱼(实际鱼由 OnFishSpawn 下发)</summary>
public const string SummonCardFishNotice = "SummonCardFishNotice";
/// <summary>全局击杀公告(真人击杀/机器人模拟)</summary>
@@ -1402,8 +1402,8 @@ public class CreateFishManager : MonoSingleton<CreateFishManager>
mc.Init(yzPathId.ToString(), cfg.times, true, yz.arrayNumber);
// ⭐ 鱼阵调试日志:输出每条子鱼生成后的旋转与缩放(排查"有的正有的反")
Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(顺序阵) pathId={yzPathId} 第{i + 1}/{yz.fishCount}条 guid={mc.guid} " +
$"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
//Debug.Log($"[鱼阵调试] 阵={yz.arrayNumber}(顺序阵) pathId={yzPathId} 第{i + 1}/{yz.fishCount}条 guid={mc.guid} " +
// $"rotation={fish.transform.eulerAngles.ToString("F1")} localScale={fish.transform.localScale.ToString("F3")}");
yield return new WaitForSeconds(1f);
}
@@ -297,6 +297,33 @@ namespace Assets.MyGame.Scripts.Net.WebResponses
public string userId;
}
/// <summary>
/// 玩家信息同步 OnPlayerInfo 数据(GetPlayerInfo 的新响应,发送给调用者)。
/// 旧事件 SendPlayerInfoResult 只返回 gold;本事件返回完整玩家信息。
/// </summary>
[Serializable]
public class OnPlayerInfoData
{
/// <summary>玩家ID</summary>
public string userId;
/// <summary>玩家昵称</summary>
public string userName;
/// <summary>金币</summary>
public long gold;
/// <summary>钻石</summary>
public int diamond;
/// <summary>VIP等级</summary>
public int viplvl;
/// <summary>炮倍</summary>
public int cannonMultiple;
/// <summary>炮台ID</summary>
public string cannonId;
/// <summary>翅膀ID</summary>
public string wingsId;
/// <summary>座位位置</summary>
public int position;
}
/// <summary>
/// 召唤卡召唤通知 SummonCardFishNotice 数据。
/// 仅用于播放召唤提示/特效/显示召唤玩家,实际鱼对象仍由 OnFishSpawn 创建。
@@ -69,6 +69,12 @@ namespace Assets.MyGame.Scripts.Net
nextHeartbeatTime = Time.realtimeSinceStartup + HeartbeatIntervalSeconds;
}
/// <summary>获取指定类型 Socket 的完整连接地址(含 baseUrl,供外部拼接查询参数,如象棋 Hub 的 ?userId=)</summary>
public string GetSocketPath(SocketType type)
{
return baseUrl + socketPaths[type];
}
private void Update()
{
if (Time.realtimeSinceStartup < nextHeartbeatTime)
@@ -90,20 +90,30 @@ namespace Assets.MyGame.Scripts.Net
case "BeginGame":
{
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<ChessGameRoom>>(msg.ToString()).arguments[0];
if (roomMainPanel != null && roomMainPanel.IsChessGameActive && va.state == 2)
{
// 对局中收到 BeginGame = 断线重连/取消挂起的房间同步:只刷新玩家数据,不重置对局
roomMainPanel.UpdateUserData(va);
}
else
{
roomMainPanel.subUIManager.CloseAll();
roomMainPanel.StartGame(msg);
roomMainPanel.UpdateUserData(va);
}
// 4.3 完整快照 + 4.4 旧房间过滤:交给面板统一处理(覆盖 roomData 并按 state 分流)
if (roomMainPanel != null) roomMainPanel.ApplyRoomSnapshot(va);
}
break;
case "RoomStateNotice":
{
// 4.3 房间状态完整快照(服务端推送 / GetRoomState 主动查询的返回)
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<ChessGameRoom>>(msg.ToString()).arguments[0];
if (roomMainPanel != null) roomMainPanel.ApplyRoomSnapshot(va);
}
break;
case "KickedFromRoom":
{
// 4.5 被踢出房间 { gameId, userId, reason?, source? }
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
string kickedUserId = va.ContainsKey("userId") ? va["userId"]?.ToString() : null;
string kickReason = va.ContainsKey("reason") ? va["reason"]?.ToString() : null;
Debug.Log($"[Chess] 被踢出房间: userId={kickedUserId}, reason={kickReason}");
if (roomMainPanel != null) roomMainPanel.OnKickedFromRoom(kickedUserId, kickReason);
}
break;
case "CreateRoomNotice":
{
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<ChessGameRoom>>(msg.ToString()).arguments[0];
@@ -168,6 +178,8 @@ namespace Assets.MyGame.Scripts.Net
break;
case "EndGameSuccess":
{
// 4.4 已退房/无房间会话时不处理旧房间的结算事件
if (roomMainPanel == null || roomMainPanel.roomData == null) break;
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg).arguments[0];
if (va == null || va == "")
{
@@ -195,29 +207,17 @@ namespace Assets.MyGame.Scripts.Net
break;
case "ExitRoomSuccess":
{
// 4.4 退出成功(自己):清空本地房间状态 → 回大厅重拉列表(0 = 重新加入大厅通知组)
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<string>>(msg).arguments[0];
if (GameManager.Instance.user.id == va)
{
WebSocketSendList.GetRoomListChineseChess(1);
if (roomMainPanel != null) roomMainPanel.ClearRoomLocalState();
WebSocketSendList.GetRoomListChineseChess(0);
}
else
{
for (int i = 0; i < roomMainPanel.roomData.users.Count; i++)
{
var item = roomMainPanel.roomData.users[i];
if (item.userId != va)
{
roomMainPanel.roomData.users.Remove(item);
roomMainPanel.UpdateUserData(roomMainPanel.roomData);
i--;
}
}
// 对方退出:房间快照由服务端随后的 BeginGame 广播覆盖(4.3),这里不做人次 -1
}
}
break;
@@ -287,6 +287,8 @@ namespace Assets.MyGame.Scripts.Net
case "GameEnd":
{
// 4.4 已退房/无房间会话时不处理旧房间的结算事件
if (roomMainPanel == null || roomMainPanel.roomData == null) break;
if (roomMainPanel != null) roomMainPanel.StopWaitCountdown(); // 游戏结束停走棋倒计时
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<Dictionary<string, object>>>(msg).arguments[0];
string winUserId = va["winUserId"]?.ToString();
@@ -731,6 +733,11 @@ namespace Assets.MyGame.Scripts.Net
WebSocketReponseList.PlayerCancelAfkNotice(msg);
break;
case "OnPlayerInfo":
// ⭐ 玩家信息同步(GetPlayerInfo 的新响应,含完整玩家数据;旧事件 SendPlayerInfoResult 只返回 gold)
WebSocketReponseList.OnPlayerInfo(msg);
break;
case "SummonCardFishNotice":
// ⭐ 召唤卡召唤通知:某玩家使用召唤卡,服务端已创建召唤鱼(实际鱼由 OnFishSpawn 下发,这里只播提示)
WebSocketReponseList.SummonCardFishNotice(msg);
@@ -489,6 +489,17 @@ namespace Assets.MyGame.Scripts.Net
EventCenter.Instance.EventTrigger<PlayerAfkNoticeData>(ConstEventString.PlayerCancelAfkNotice, data);
}
/// <summary>
/// 玩家信息同步 OnPlayerInfo(GetPlayerInfo 的新响应,发送给调用者)。
/// 旧事件 SendPlayerInfoResult 只返回 gold;本事件返回完整玩家信息,UI 层通过 EventCenter 消费。
/// </summary>
public static void OnPlayerInfo(string msg)
{
var data = JsonConvert.DeserializeObject<WebSocketResponseBase<OnPlayerInfoData>>(msg).arguments[0];
if (data == null) return;
EventCenter.Instance.EventTrigger<OnPlayerInfoData>(ConstEventString.OnPlayerInfo, data);
}
/// <summary>
/// 召唤卡召唤通知 SummonCardFishNotice。
/// 某玩家使用召唤卡(40006),服务端已创建 Boss鱼(飞机场)/黄金鱼(其他场次)。
@@ -80,4 +80,16 @@ public partial class WebSocketSendList
{
WebSocketManager.Instance.Send(SocketType.chess, "{\"type\":1,\"target\":\"CancelPlayerAfk\",\"invocationId\":\"1\",\"arguments\":[\"" + gameId + "\",\"" + userId + "\"]}");
}
/// <summary>房主踢人(4.5):开局前将目标玩家踢出房间,被踢者收到 KickedFromRoom</summary>
public static void KickPlayerChineseChess(string gameId, string targetUserId)
{
WebSocketManager.Instance.Send(SocketType.chess, "{\"type\":1,\"target\":\"KickPlayer\",\"invocationId\":\"1\",\"arguments\":[\"" + gameId + "\",\"" + targetUserId + "\"]}");
}
/// <summary>主动查询房间状态(4.3):服务端返回 RoomStateNotice(room) 完整快照,用于重连/断线超时后校准本地状态</summary>
public static void GetRoomStateChineseChess(string gameId)
{
WebSocketManager.Instance.Send(SocketType.chess, "{\"type\":1,\"target\":\"GetRoomState\",\"invocationId\":\"1\",\"arguments\":[\"" + gameId + "\"]}");
}
}
@@ -261,22 +261,22 @@ public partial class WebSocketSendList
}
/// <summary>
/// 玩家挂机(对应服务端 SetPlayerAfkNEW)
/// 玩家挂机(对应服务端 SetPlayerAfk)
/// 服务端会向房间组广播 PlayerAfkNotice 事件
/// </summary>
public static void SetPlayerAfk(string roomId, string userId)
{
var sendObj = new WebSocketSendList("SetPlayerAfkNEW", roomId, userId);
var sendObj = new WebSocketSendList("SetPlayerAfk", roomId, userId);
WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj));
}
/// <summary>
/// 取消挂机(对应服务端 CancelPlayerAfkNEW)
/// 服务端会向房间组广播 PlayerCancelAfkNotice 事件,并通过 OnFishSpawn(type=sync) 同步当前鱼
/// 取消挂机(对应服务端 CancelPlayerAfk)
/// 服务端会向房间组广播 PlayerCancelAfkNotice 事件,并通过 RoomInfo(activeFishes) 同步当前鱼
/// </summary>
public static void CancelPlayerAfk(string roomId, string userId)
{
var sendObj = new WebSocketSendList("CancelPlayerAfkNEW", roomId, userId);
var sendObj = new WebSocketSendList("CancelPlayerAfk", roomId, userId);
WebSocketManager.Instance.Send(SocketType.game, JsonConvert.SerializeObject(sendObj));
}
@@ -894,6 +894,7 @@ MonoBehaviour:
btn_SheZhi: {fileID: 1337329277101656982}
btn_GuiZe: {fileID: 4683566954994342685}
btn_StartGame: {fileID: 7784025018183944699}
btn_TiRen: {fileID: 4110785601629470162}
img_DengDaiZhong: {fileID: 8918328693605437385}
btn_FanHuiDaTing: {fileID: 8119276241258304471}
tmp_Name_Bottom: {fileID: 125596079223217066}
@@ -1126,6 +1127,127 @@ MonoBehaviour:
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &3526777765374275067
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3099333695345336700}
- component: {fileID: 1347967214700038226}
- component: {fileID: 755834463141100930}
- component: {fileID: 4110785601629470162}
m_Layer: 5
m_Name: TiRen
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3099333695345336700
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3526777765374275067}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1548431137010129466}
m_Father: {fileID: 6915003179490517050}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 67.6864, y: -62}
m_SizeDelta: {x: 137.62, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1347967214700038226
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3526777765374275067}
m_CullTransparentMesh: 1
--- !u!114 &755834463141100930
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3526777765374275067}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &4110785601629470162
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3526777765374275067}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 755834463141100930}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &3533895123476321566
GameObject:
m_ObjectHideFlags: 0
@@ -2629,6 +2751,7 @@ RectTransform:
- {fileID: 2435241360537787627}
- {fileID: 8837399197562350486}
- {fileID: 1317669282810263189}
- {fileID: 3099333695345336700}
m_Father: {fileID: 2713217087351076567}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
@@ -2870,6 +2993,141 @@ RectTransform:
m_AnchoredPosition: {x: -267.8, y: 64.3}
m_SizeDelta: {x: 100, y: 100}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!1 &7812771172489494558
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1548431137010129466}
- component: {fileID: 4652176340497631044}
- component: {fileID: 4888819185582453806}
m_Layer: 5
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1548431137010129466
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7812771172489494558}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 3099333695345336700}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4652176340497631044
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7812771172489494558}
m_CullTransparentMesh: 1
--- !u!114 &4888819185582453806
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7812771172489494558}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: "\u8E22\u4EBA"
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 5d4a713fdd931db4c9ce49f068b54e0a, type: 2}
m_sharedMaterial: {fileID: 3593458263369068374, guid: 5d4a713fdd931db4c9ce49f068b54e0a,
type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_enableWordWrapping: 0
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 1
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &8038616329731275413
GameObject:
m_ObjectHideFlags: 0
@@ -41,8 +41,11 @@ public class ChineseChessMainPanel : GameMainPanel_Base
public Button btn_SheZhi;
public Button btn_GuiZe;
public Button btn_StartGame;
public Button btn_TiRen; // 踢人按钮(拖拽:房主 + 开局前 + 双方在房间时自动显示,其余情况自动隐藏)
public Image img_DengDaiZhong;
public ChessGameRoom roomData;
/// <summary>4.4 已退出的房间 ID:退房后忽略该房间的后续推送(重新进房时由 MarkJoiningRoom 解除)</summary>
private string lastExitedRoomId;
public static Dictionary<string,ChessGameRoom> dic_RoomData=new Dictionary<string, ChessGameRoom>();
public Button btn_FanHuiDaTing;
[Header("Bottom")]
@@ -223,7 +226,71 @@ public class ChineseChessMainPanel : GameMainPanel_Base
public void StartGame(string msg)
{
var va = JsonConvert.DeserializeObject<WebSocketResponseBase<ChessGameRoom>>(msg.ToString());
ChessGameRoom chessGameRoom = va.arguments[0];
StartGameRoom(va.arguments[0]);
}
/// <summary>
/// 4.3 房间状态完整快照:BeginGame / RoomStateNotice / GetRoomState 返回统一走这里。
/// 用服务端房间对象整体覆盖本地状态(users/state/gold/createUserId/isGameEnded/timeoutSeconds/pendingWinUserId),
/// 不做"人数 ±1"式增量更新。
/// </summary>
public void ApplyRoomSnapshot(ChessGameRoom room)
{
if (room == null || string.IsNullOrEmpty(room.roomId)) return;
// 4.4 防串房:忽略已退出的旧房间推送
if (!string.IsNullOrEmpty(lastExitedRoomId) && room.roomId == lastExitedRoomId)
{
Debug.Log($"[Chess] 忽略已退出房间的快照: {room.roomId}");
return;
}
// 4.4 防串房:本地在 A 房间时忽略 B 房间的推送
if (roomData != null && !string.IsNullOrEmpty(roomData.roomId) && roomData.roomId != room.roomId)
{
Debug.Log($"[Chess] 忽略其他房间快照: {room.roomId}(当前: {roomData.roomId})");
return;
}
bool wasActive = IsChessGameActive;
roomData = room; // 4.3 整体覆盖
if (wasActive && room.state == 2 && !room.isGameEnded)
{
// 4.1 断线重连/取消挂起的恢复同步:只刷新双方信息,不重置正在进行的棋局
UpdateUserData(room);
return;
}
if (wasActive)
{
// 4.1 房间已被服务端重置(双断线)或已结束:清除本地对局,按快照回到对应阶段
ClearChessGame();
}
subUIManager.CloseAll();
StartGameRoom(room);
}
/// <summary>重新进入指定房间前调用:解除对该房间的"旧房间推送过滤"(建房/进房面板发送请求时用)</summary>
public void MarkJoiningRoom(string roomId)
{
if (!string.IsNullOrEmpty(roomId) && roomId == lastExitedRoomId)
lastExitedRoomId = null;
}
/// <summary>
/// 4.4 清空本地房间状态:gameId、棋盘、双方玩家、准备状态、结算状态
/// </summary>
public void ClearRoomLocalState()
{
if (roomData != null) lastExitedRoomId = roomData.roomId; // 记录,忽略该房间后续推送
ClearChessGame();
roomData = null;
RefreshKickButtonVisible(); // 已退房 → 隐藏踢人按钮
}
private void StartGameRoom(ChessGameRoom chessGameRoom)
{
roomData = chessGameRoom; // 记录房间数据(开象棋对局时要取阵营)
StartHeartbeat(); // 房间心跳(更新活跃时间 + 断线超时检查)
UpdateUserData(chessGameRoom);
@@ -327,6 +394,7 @@ public class ChineseChessMainPanel : GameMainPanel_Base
LoadAvatar(img_Title_Top, item.userAvatar);
}
}
RefreshKickButtonVisible(); // 房间数据刷新 → 同步踢人按钮显隐
}
/// <summary>
@@ -371,12 +439,31 @@ public class ChineseChessMainPanel : GameMainPanel_Base
base.Awake();
btn_FanHuiDaTing.onClick.AddListener(() =>
{
UIManager.Instance.Close(UIPanel.MainGamePanelZGXQ);
if (roomData != null)
{
// 4.4 在房间中:必须主动调用退出房间,不能只断开连接
WebSocketSendList.ExitRoomChineseChess(roomData.roomId, MyChessUserId);
ClearRoomLocalState(); // 立即清 gameId/棋盘/双方/准备/结算状态
subUIManager.CloseAll(); // 关结算等子面板
WebSocketSendList.GetRoomListChineseChess(0); // 回大厅重拉列表(响应自动刷新选房界面,0=重新加入大厅通知组)
}
else
{
// 4.4 已在选房界面:关闭象棋面板(OnHide 兜底再退一次房)
UIManager.Instance.Close(UIPanel.MainGamePanelZGXQ);
}
});
btn_GuiZe.onClick.AddListener(() =>
{
subUIManager.ShowAsync(UIPanel.ZGXQIntroduce);
});
// 4.5 踢人按钮:房主 + 开局前 + 对方在房间时才显示(显隐由 RefreshKickButtonVisible 控制)
if (btn_TiRen != null)
{
btn_TiRen.onClick.RemoveAllListeners();
btn_TiRen.onClick.AddListener(OnKickButtonClicked);
btn_TiRen.gameObject.SetActive(false); // 默认隐藏
}
}
protected override void OnShow(object args = null)
{
@@ -384,6 +471,7 @@ public class ChineseChessMainPanel : GameMainPanel_Base
// 订阅用户数据变化:结算/领奖后 LoginHub AssetUpdated 会触发,实时刷新房间金币
GameManager.Instance.OnUserDataChanged -= OnUserDataChangedHandler;
GameManager.Instance.OnUserDataChanged += OnUserDataChangedHandler;
RefreshKickButtonVisible(); // 进面板默认隐藏踢人按钮(防上次残留)
// 回到选房间界面时显示等待中(连带父对象)
SetDengDaiZhongActive(true);
subUIManager.ShowAsync(UIPanel.ZGXQDistrict, args);
@@ -402,6 +490,8 @@ public class ChineseChessMainPanel : GameMainPanel_Base
}
public void IsShowHint(string id,int type)
{
// 4.4 只处理当前对手发起的操作请求(旧房间/陌生 userId 忽略)
if (roomData == null || id != OpponentChessUserId) return;
if (GameManager.Instance.user.BaseInfo.number!=id)
{
// 悔棋回执:直接处理,不弹窗
@@ -489,6 +579,7 @@ public class ChineseChessMainPanel : GameMainPanel_Base
chessGameManager.OnGameOver += OnChessGameOver;
chessGameManager.OnTurnChanged += OnChessTurnChanged; // 要在 StartGame 前订阅(开局红先会触发一次)
chessGameManager.StartGame(myCamp);
RefreshKickButtonVisible(); // 对局开始 → 隐藏踢人按钮
if (chessBoard == null) CreateDefaultChessBoard();
chessBoard.Init(chessGameManager, myCamp);
@@ -646,6 +737,7 @@ public class ChineseChessMainPanel : GameMainPanel_Base
private long opponentAfkStartTimeMs; // 对方挂起开始时间(北京时间毫秒)
private Coroutine afkMonitorCoroutine;
private Coroutine heartbeatCoroutine;
private Coroutine disconnectCountdownCoroutine; // 对方断线 60s 离线倒计时
/// <summary>是否处于对局中(挂起/断线/重连处理只对进行中的对局生效)</summary>
public bool IsChessGameActive => chessGameManager != null && !chessGameManager.isGameOver;
@@ -701,6 +793,10 @@ public class ChineseChessMainPanel : GameMainPanel_Base
}
// 都没超时 → 继续对局(对方挂起监控继续跑)
// 4.1 兜底:主动查询房间真实状态,防止服务端已判负/重置但推送在后台丢失
// (快照分流:还在对局 → 只刷新数据;已判负/重置 → 自动清本地回对应界面)
if (roomData != null) WebSocketSendList.GetRoomStateChineseChess(roomData.roomId);
// 恢复走棋倒计时
OnChessTurnChanged(chessGameManager.currentTurn);
}
@@ -776,23 +872,102 @@ public class ChineseChessMainPanel : GameMainPanel_Base
subUIManager.ShowAsync(iWin ? UIPanel.ZGXQWin : UIPanel.ZGXQDefeated);
}
/// <summary>Disconnect:对方断线 → 停倒计时 + 提示(服务端超时未重连自动判负推 GameEnd)</summary>
/// <summary>Disconnect:对方断线 → 停走棋倒计时 + 上方显示离线倒计时(60s 内未重连由服务端判负,超时后主动查房间状态兜底)</summary>
public void OnOpponentDisconnected(string userId, int timeoutSeconds)
{
if (!IsChessGameActive || userId != OpponentChessUserId) return;
StopWaitCountdown();
if (chessBoard != null)
chessBoard.ShowToast($"对方断线,{Mathf.Max(1, timeoutSeconds)}秒内未重连将判负");
if (disconnectCountdownCoroutine != null) StopCoroutine(disconnectCountdownCoroutine);
disconnectCountdownCoroutine = StartCoroutine(DisconnectCountdownRoutine(Mathf.Max(1, timeoutSeconds)));
}
/// <summary>Reconnect:对方重连 → 恢复走棋倒计时</summary>
/// <summary>对方断线倒计时:显示离线状态和剩余秒数;倒计时归零后主动 GetRoomState 校准(房间被重置/已判负时快照分流自动处理)</summary>
private IEnumerator DisconnectCountdownRoutine(int totalSeconds)
{
int remaining = totalSeconds;
while (remaining >= 0)
{
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = $"对方断线 {remaining / 60:D2}:{remaining % 60:D2}";
if (tmp_WaitTime_Bottom != null) tmp_WaitTime_Bottom.text = "";
yield return new WaitForSeconds(1f);
remaining--;
}
disconnectCountdownCoroutine = null;
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
// 4.1 超过重连时间:主动查询房间真实状态(服务端超时会推 GameEnd;房间已释放/重置则 RoomStateNotice 快照分流带回大厅)
if (IsChessGameActive && roomData != null)
WebSocketSendList.GetRoomStateChineseChess(roomData.roomId);
}
/// <summary>Reconnect:对方重连 → 停断线倒计时 + 恢复走棋倒计时</summary>
public void OnOpponentReconnected(string userId)
{
if (!IsChessGameActive || userId != OpponentChessUserId) return;
if (disconnectCountdownCoroutine != null)
{
StopCoroutine(disconnectCountdownCoroutine);
disconnectCountdownCoroutine = null;
if (tmp_WaitTime_Top != null) tmp_WaitTime_Top.text = "";
}
if (chessBoard != null) chessBoard.ShowToast("对方已重连");
OnChessTurnChanged(chessGameManager.currentTurn);
}
#endregion
#region 踢人(4.5:房主开局前可踢人,被踢端完整处理)
/// <summary>4.5 被踢出房间 KickedFromRoom({ gameId, userId, reason?, source? }):只处理自己被踢</summary>
public void OnKickedFromRoom(string userId, string reason)
{
if (userId != MyChessUserId) return;
// 停止计时/棋盘/输入 → 清空房间本地状态 → 回大厅重拉列表
ClearRoomLocalState();
subUIManager.CloseAll();
WebSocketSendList.GetRoomListChineseChess(0);
// 优先显示服务端 reason,为空显示默认文案
UIManager.Instance.ShowBoardError(string.IsNullOrEmpty(reason) ? "已被移出房间" : reason);
}
/// <summary>房主点击踢人按钮(开局前):3 秒内二次点击确认踢出(4.5 KickPlayer)</summary>
private float lastKickClickTime;
private void OnKickButtonClicked()
{
// 仅房主 + 开局前 + 对方在房间内时可踢
if (roomData == null || roomData.users == null || roomData.users.Count < 2) return;
if (IsChessGameActive || roomData.state == 2) return; // 对局中/服务端已开局不可踢
if (roomData.users[0].userId != MyChessUserId) return; // 第一个进房的是房主,只有房主能踢
string oppId = OpponentChessUserId;
if (string.IsNullOrEmpty(oppId)) return;
if (Time.realtimeSinceStartup - lastKickClickTime > 3f)
{
lastKickClickTime = Time.realtimeSinceStartup;
UIManager.Instance.ShowBoardError("再次点击踢人按钮确认踢出对方");
return;
}
WebSocketSendList.KickPlayerChineseChess(roomData.roomId, oppId);
}
/// <summary>踢人按钮显隐:房主(第一个进房)+ 开局前 + 双方在房间时显示,其余隐藏(房间数据每次刷新时调用)</summary>
private void RefreshKickButtonVisible()
{
if (btn_TiRen == null) return;
bool show = roomData != null && roomData.users != null
&& roomData.users.Count >= 2 // 对方在房间
&& roomData.users[0].userId == MyChessUserId // 第一个进房的是房主
&& roomData.state != 2 // 服务端未开局
&& !IsChessGameActive; // 本地对局未开始
btn_TiRen.gameObject.SetActive(show);
}
#endregion
/// <summary>服务端结算通知(EndGameSuccess/GameEnd)→ 本地对局同步置结束,防止后台方回前台后对局还在跑</summary>
public void MarkChessGameOverByServer()
{
@@ -878,8 +1053,8 @@ public class ChineseChessMainPanel : GameMainPanel_Base
catch { return; }
if (net == null || string.IsNullOrEmpty(net.userId)) return;
// 服务器回声/自己发的帧直接忽略
if (net.userId == GameManager.Instance.user.BaseInfo.number) return;
// 4.4 只处理当前对手的走子帧(自己的回声、旧房间、陌生 userId 一并忽略)
if (net.userId != OpponentChessUserId) return;
if (net.msgType == 1) // 对方走棋
{
@@ -900,6 +1075,11 @@ public class ChineseChessMainPanel : GameMainPanel_Base
StopWaitCountdown(); // 停走棋倒计时
StopAfkMonitor(); // 停挂起监控
StopHeartbeat(); // 停房间心跳
if (disconnectCountdownCoroutine != null) // 停对方断线倒计时
{
StopCoroutine(disconnectCountdownCoroutine);
disconnectCountdownCoroutine = null;
}
isMyAfk = false;
isOpponentAfk = false;
if (chessBoard != null) chessBoard.Clear();
@@ -911,7 +1091,6 @@ public class ChineseChessMainPanel : GameMainPanel_Base
}
}
#endregion
public void Start()
{
@@ -977,6 +1156,12 @@ public class ChineseChessMainPanel : GameMainPanel_Base
protected override void OnHide()
{
base.OnHide();
// 4.4 任何关闭路径都确保已退房(连接尚未关闭,能发出去;已退过则 roomData 为空跳过)
if (roomData != null)
{
WebSocketSendList.ExitRoomChineseChess(roomData.roomId, MyChessUserId);
ClearRoomLocalState();
}
// 退订用户数据变化
GameManager.Instance.OnUserDataChanged -= OnUserDataChangedHandler;
// 清理象棋对局
@@ -65,6 +65,8 @@ public class UIChineseChessCreateRoom : SubUIBase
UIManager.Instance.ShowBoardError("密码格式不正确");
return;
}
// 4.4 重新进入该房间:解除"已退出房间"的推送过滤
(uIManager.parentUIBase as ChineseChessMainPanel)?.MarkJoiningRoom(v.roomId);
WebSocketSendList.CreateRoomChineseChess(new Assets.MyGame.Scripts.Net.WebResponses.ChineseChess.ChineseChessGameClassList.CreateRoomParam
{
gameType = gameType,
@@ -1,4 +1,4 @@
using Assets.MyGame.Scripts.Net.WebResponses.ChineseChess;
using Assets.MyGame.Scripts.Net.WebResponses.ChineseChess;
using System.Collections;
using System.Collections.Generic;
using TMPro;
@@ -28,6 +28,8 @@ public class UIChineseChessJoinRoom : SubUIBase
{
if (tMPInput_Password.text.Length == 6)
{
// 4.4 重新进入该房间:解除"已退出房间"的推送过滤
(uIManager.parentUIBase as ChineseChessMainPanel)?.MarkJoiningRoom(v.roomId);
WebSocketSendList.JoinGameChineseChess(v.roomId, tMPInput_Password.text, GameManager.Instance.user.BaseInfo.number);
}
else
+4 -2
View File
@@ -96,10 +96,12 @@ public class UIHomeMain : UIBase
ZGXQButton.onClick.AddListener(() =>
{
//subUIManager.ShowAsync(UIPanel.SelectRoomPenel, GameType.ZGXQ);
WebSocketManager.Instance.Connect(SocketType.chess, () =>{
// 4.1 协议要求:连接象棋 Hub 必须携带当前玩家 ID /chessgamehub?userId={userId}
string chessUrl = WebSocketManager.Instance.GetSocketPath(SocketType.chess) + "?userId=" + GameManager.Instance.user.BaseInfo.number;
WebSocketManager.Instance.Connect(SocketType.chess, chessUrl, () =>{
// WebSocketManager.OnOpen 已处理首次 GetRoomList,这里不再重复调用
});
});
XJMHButton.onClick.AddListener(() =>
{