Unity2D 五子棋 + Photon聯網雙人對戰

開發環境配置

Unity版本2022.3

創建Photon賬號以及申請Photon中國區服務

官網申請賬號:Multiplayer Game Development Made Easy Photon Engine

在這里插入圖片描述

中國區服務: 光子引擎photonengine中文站 成都動聯無限科技有限公司(vibrantlink.com)

在這里插入圖片描述

導入PUN2插件以及配置

Unity商城地址: PUN 2 - FREE 網絡 Unity Asset Store

導入PUN2的時候會有一個填寫APPID或者郵箱的窗口,跳過就行。

在這里插入圖片描述

App ID PUN 在申請Photon服務器的時候會有,以及在申請中國區服務的時候郵箱里也會有。

App Version 應該不寫也沒事。

Fixed Region 填寫CN。

修改代碼 Assets/Photon/PhotonRealtime/Code/LoadBalancingClient.cs

將上方一行改為下方一行。

// public string NameServerHost = "ns.photonengine.io";
public string NameServerHost = "ns.photonengine.cn";

構建場景 制作聯網的游戲對象預制體

資源處理

隨便找三張圖湊合一下,將圖片修改為Sprite。由于圖片自帶陰影的問題,樞軸不在棋子中心,稍微調整一下。

在這里插入圖片描述

調整邊框,改為像素模式,然后將藍色圓圈(樞軸)移動到上下左右四個中心點,觀察右下角X,Y的值,求和除以2即是中心點。

將棋盤棋子放入場景中調整為合適大小,棋子大小0.4看起來還行,通過對齊兩個棋子在棋盤網格上時的樞軸,得出棋盤網格大小為0.35。

在這里插入圖片描述

其他設置

如果創建的是3D項目,需要改相機的設置。

在這里插入圖片描述

一定要改為Orthographic,不然鼠標點擊時位置處理會出問題。

游戲場景大小設置為480*800(其他也行,場景中物體的大小適當改改就行,還有坐標計算時的零點會不一樣):

在這里插入圖片描述

創建Player游戲對象(空對象),黑棋白棋游戲對象(將棋子圖片拖拽進場景即可,前提是已經改為Sprite),將棋子的Order In Layer 設置的比棋盤大,確保棋子渲染在上層,不被遮擋。

在這里插入圖片描述

給Player,棋子,添加Photon View組件:Player不做處理,棋子額外設置

在這里插入圖片描述

將Observable Search設置為Manual,并將他們的Transform拖拽到Observed Components中。此設置為同步棋子的transform組件,后邊在生成棋子的時候讓兩邊客戶端的棋子位置一致。

將Player和黑白棋子保存到Resources中保存為預制體(Prefab),存在這個位置才能讓PUN來控制生成需要的游戲對象,并且確保PUN在通知各個客戶端生成游戲對象時物體信息的統一。

核心功能實現

Chess.cs

using UnityEngine;
using Photon.Pun;// 棋子類型枚舉,黑色或白色
public enum ChessType { Black,White}public class Chess : MonoBehaviour
{public int row; // 棋子所在行public int column;  // 棋子所在列public ChessType chessType = ChessType.Black;   // 棋子類型[PunRPC]public void SetPositionInfo(int[] rowColumn){row = rowColumn[0];column = rowColumn[1];}
}

給棋子添加腳本Chess.cs。我習慣了Chess,準確翻譯應該是Piece。

然后給白棋預制體設置默認為白棋,

在這里插入圖片描述

Player.cs

給玩家添加Player.cs腳本。如果在場景中給Player添加腳本,記得覆蓋修改到預制體。

玩家落子時通過坐標計算位置,所以需要有一個坐標軸,這里以棋盤左下角為原點,通過之前處理棋子時得知棋盤左下角的坐標是(-2.45,-2.45,0),還有格子寬度為0.35,所以玩家腳本保存這些信息。

首先要確保游戲開始,并且只能控制自己。(這里判斷是否所有人都準備的代碼應該有更合適的寫法,不應該每次update都Find一遍所有Player,似乎可以利用NetWorkManager中的字典,查一個最多只有兩個鍵值對的字典一定比在場景中FindObjectsOfType遍歷一遍所有游戲對象快,懶得改了,算了。)

// 只允許控制自己的玩家對象
if (!photonView.IsMine) return;
// 檢查是否輪到當前玩家回合
if (netWorkManager.playerTurn != chessType) return;
// 驗證所有玩家是否都已準備
var players = GameObject.FindObjectsOfType<Player>();
foreach (var player in players)
{if (player.playerState != PlayerState.Ready) return;
}
// 更新游戲狀態為開始
netWorkManager.gameState = GameState.Start;
// 游戲未開始時禁止操作
if (netWorkManager.gameState != GameState.Start) return;

計算正確的落子位置,(棋子的保存也許應該保存在一個15*15的二維表中,那樣同樣能避免反復執行FindObjectsOfType遍歷所有棋子,棋子中的RPC可以刪掉,然后在Player寫一個RPC同步二維表情況)

在這里插入圖片描述

// 將屏幕坐標轉換為世界坐標
mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
// 計算相對于棋盤原點的偏移量
offset = mousePos - zeroPosition;// 計算點擊的棋盤行列號
column = (int)Mathf.Round(offset.x / cellWidth);  // X軸對應列號
row = (int)Mathf.Round(offset.y / cellWidth);     // Y軸對應行號
rowColumn[0] = row;  // 存儲為數組[行,列]
rowColumn[1] = column;// 落子合法性驗證 ------------------------
// 邊界檢查(15x15棋盤)
if (row < 0 || row > 14 || column < 0 || column > 14) return;
// 檢查是否已有棋子
chessList = GameObject.FindObjectsOfType<Chess>().ToList();
foreach (var chess in chessList)
{if (chess.row == row && chess.column == column) return;
}// 計算棋子生成的世界坐標
generatePos = new Vector3(column * cellWidth,  // X軸位置 = 列號 * 格寬row * cellWidth,     // Y軸位置 = 行號 * 格寬 0) + zeroPosition;   // 加上棋盤原點偏移// 生成棋子對象 --------------------------
Chess currentChess = null;
if (chessType == ChessType.Black && blackChess != null)
{// 生成黑棋并同步newChess = PhotonNetwork.Instantiate(blackChess.name, generatePos, Quaternion.identity);newChess.GetComponent<PhotonView>().RPC("SetPositionInfo", RpcTarget.All, rowColumn);currentChess = newChess.GetComponent<Chess>();
}
else if (whiteChess != null)
{// 生成白棋并同步newChess = PhotonNetwork.Instantiate(whiteChess.name, generatePos, Quaternion.identity);newChess.GetComponent<PhotonView>().RPC("SetPositionInfo", RpcTarget.All, rowColumn);currentChess = newChess.GetComponent<Chess>();
}

RPC方式同步不同客戶端棋子的行列值,在Player中調用了Chess.cs的代碼來設置棋子所在的行列。

newChess.GetComponent<PhotonView>().RPC("SetPositionInfo", RpcTarget.All, rowColumn);
using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using System.Linq;// 玩家狀態枚舉:未準備/已準備
public enum PlayerState
{NotReady,Ready
}public class Player : MonoBehaviour
{// 棋盤相關參數public Vector3 zeroPosition;    // 棋盤左下角原點坐標(世界坐標系)public float cellWidth;         // 每個棋格寬度(世界單位)public ChessType chessType = ChessType.Black;   // 玩家棋子顏色(黑/白)private List<Chess> chessList = new List<Chess>();   // 已放置棋子列表// 網絡組件[HideInInspector]public PhotonView photonView;  // 當前玩家對象的PhotonView組件private NetWorkManager netWorkManager;          // 場景中的網絡管理器// 棋子放置相關變量private Vector3 generatePos;    // 棋子生成位置(世界坐標)private int row;                // 當前點擊位置的行號(0-14)private int column;             // 當前點擊位置的列號(0-14)private int[] rowColumn = new int[2];   // 行列號的數組形式(用于RPC參數)// 輸入計算相關private Vector3 mousePos;   // 鼠標點擊位置(屏幕坐標系)private Vector3 offset;     // 點擊位置相對于棋盤原點的偏移量// 棋子預制體public GameObject blackChess;   // 黑棋預制體public GameObject whiteChess;   // 白棋預制體private GameObject newChess;    // 最新生成的棋子對象// 玩家狀態public PlayerState playerState = PlayerState.NotReady;  // 玩家準備狀態void Start(){photonView = GetComponent<PhotonView>();netWorkManager = GameObject.FindObjectOfType<NetWorkManager>();// 初始化玩家UI顯示if (photonView.IsMine){netWorkManager.SetSelfText(chessType);}else{netWorkManager.SetHostilefText(chessType);}// 注冊玩家到網絡管理器Photon.Realtime.Player photonPlayer = photonView.Owner;netWorkManager.RegisterPlayer(photonPlayer.ActorNumber, this);}void Update(){// 玩家控制權驗證 --------------------------// 只允許控制自己的玩家對象if (!photonView.IsMine) return;// 檢查是否輪到當前玩家回合if (netWorkManager.playerTurn != chessType) return;// 驗證所有玩家是否都已準備var players = GameObject.FindObjectsOfType<Player>();foreach (var player in players){if (player.playerState != PlayerState.Ready) return;}// 更新游戲狀態為開始netWorkManager.gameState = GameState.Start;// 游戲未開始時禁止操作if (netWorkManager.gameState != GameState.Start) return;// 鼠標點擊處理 ------------------------------if (Input.GetMouseButtonDown(0)){// 坐標轉換計算 --------------------------// 將屏幕坐標轉換為世界坐標mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);// 計算相對于棋盤原點的偏移量offset = mousePos - zeroPosition;// 計算點擊的棋盤行列號column = (int)Mathf.Round(offset.x / cellWidth);  // X軸對應列號row = (int)Mathf.Round(offset.y / cellWidth);     // Y軸對應行號rowColumn[0] = row;  // 存儲為數組[行,列]rowColumn[1] = column;// 落子合法性驗證 ------------------------// 邊界檢查(15x15棋盤)if (row < 0 || row > 14 || column < 0 || column > 14) return;// 檢查是否已有棋子chessList = GameObject.FindObjectsOfType<Chess>().ToList();foreach (var chess in chessList){if (chess.row == row && chess.column == column) return;}// 計算棋子生成的世界坐標generatePos = new Vector3(column * cellWidth,  // X軸位置 = 列號 * 格寬row * cellWidth,     // Y軸位置 = 行號 * 格寬 0) + zeroPosition;   // 加上棋盤原點偏移// 生成棋子對象 --------------------------Chess currentChess = null;if (chessType == ChessType.Black && blackChess != null){// 生成黑棋并同步newChess = PhotonNetwork.Instantiate(blackChess.name, generatePos, Quaternion.identity);newChess.GetComponent<PhotonView>().RPC("SetPositionInfo", RpcTarget.All, rowColumn);currentChess = newChess.GetComponent<Chess>();}else if (whiteChess != null){// 生成白棋并同步newChess = PhotonNetwork.Instantiate(whiteChess.name, generatePos, Quaternion.identity);newChess.GetComponent<PhotonView>().RPC("SetPositionInfo", RpcTarget.All, rowColumn);currentChess = newChess.GetComponent<Chess>();}// 游戲邏輯處理 --------------------------// 播放落子音效netWorkManager.GetComponent<PhotonView>().RPC("PlayMarkingAudio", RpcTarget.All);// 勝負判定chessList = GameObject.FindObjectsOfType<Chess>().ToList();bool isFive = JudgeFiveChess(chessList, currentChess);if (isFive){// 游戲結束處理netWorkManager.GetComponent<PhotonView>().RPC("GameOver", RpcTarget.All, currentChess.chessType);netWorkManager.GetComponent<PhotonView>().RPC("ReSetGame", RpcTarget.All);return;}// 切換回合netWorkManager.GetComponent<PhotonView>().RPC("ChangeTurn", RpcTarget.All);}}/// <summary>/// 獲取當前玩家的可序列化狀態數據(用于網絡同步)/// </summary>/// <returns>/// 返回包含玩家關鍵狀態的對象數組:/// [0] ChessType - 棋子顏色類型/// [1] PlayerState - 玩家準備狀態/// </returns>public object[] GetPlayerState(){return new object[] {chessType,       // 玩家當前棋子顏色(Black/White)playerState      // 玩家準備狀態(NotReady/Ready)};}/// <summary>/// [PunRPC] 設置玩家棋子類型(網絡同步方法)/// </summary>[PunRPC]public void SetChessType(ChessType type){chessType = type;  // 設置當前玩家棋子顏色}/// <summary>/// [PunRPC] 設置玩家準備狀態(網絡同步方法)/// </summary>/// <remarks>/// 執行流程:/// 1. 修改本地玩家準備狀態/// 2. 更新Photon網絡自定義屬性(自動同步到其他客戶端)/// 3. 更新本地和對手的UI顯示/// </remarks>[PunRPC]public void SetPlayerReady(){// 修改本地狀態為已準備playerState = PlayerState.Ready;// 設置Photon自定義屬性(自動同步到所有客戶端)ExitGames.Client.Photon.Hashtable props = new ExitGames.Client.Photon.Hashtable{{ "IsReady", true }  // 使用Hashtable鍵值對存儲準備狀態};PhotonNetwork.LocalPlayer.SetCustomProperties(props); // 觸發OnPlayerPropertiesUpdate回調// 更新UI顯示if (photonView.IsMine){// 更新自己的準備狀態顯示netWorkManager.selfReadyText.text = "已準備";}else{// 更新對手的準備狀態顯示netWorkManager.hostileReadyText.text = "已準備";}}/// <summary>/// 判斷是否形成五子連珠(核心勝負判定算法)/// </summary>/// <param name="chessList">棋盤上所有棋子列表</param>/// <param name="currentChess">當前剛落下的棋子</param>/// <returns>true表示五子連珠達成,false表示未達成</returns>/// <remarks>/// 檢測邏輯:/// 1. 篩選出當前玩家顏色的所有棋子/// 2. 從當前棋子出發,向8個方向遞歸檢測連續棋子/// 3. 合并相反方向的棋子數量,判斷是否達到5連/// </remarks>bool JudgeFiveChess(List<Chess> chessList, Chess currentChess){bool result = false;// 篩選當前玩家顏色的棋子(優化點:可緩存此列表避免重復篩選)List<Chess> currentChessTypeList = chessList.Where(en => en.chessType == chessType).ToList();// 八方向檢測(實際只需四個軸線方向的檢測)List<Chess> upList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.Up);     // 正上方List<Chess> downList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.Down); // 正下方List<Chess> leftList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.Left); // 正左方List<Chess> rightList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.Right);// 正右方List<Chess> leftUpList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.LeftUp);    // 左上方List<Chess> rightDownList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.RightDown);// 右下方List<Chess> leftDownList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.LeftDown);  // 左下方List<Chess> rightUpList = GetSameChessByDirection(currentChessTypeList, currentChess, ChessDirection.RightUp);    // 右上方// 四軸線勝負判定(垂直/水平/左上右下斜線/左下右上斜線)if (upList.Count + downList.Count + 1 >= 5 ||          // 垂直方向(當前棋子+上方+下方)leftList.Count + rightList.Count + 1 >= 5 ||        // 水平方向(當前棋子+左方+右方)leftUpList.Count + rightDownList.Count + 1 >= 5 ||  // 主斜線(當前棋子+左上方+右下方)leftDownList.Count + rightUpList.Count + 1 >= 5)    // 副斜線(當前棋子+左下方+右上方){result = true;}return result;}/// <summary>/// 遞歸獲取指定方向上的連續同色棋子(深度優先搜索)/// </summary>/// <param name="currentChessTypeList">當前玩家顏色的所有棋子</param>/// <param name="currentChess">當前檢測的基準棋子</param>/// <param name="direction">檢測方向(八方向枚舉)</param>/// <returns>沿指定方向的連續棋子列表(不包含當前棋子)</returns>/// <remarks>/// 實現原理:/// 1. 根據方向參數確定相鄰棋子的行列偏移量/// 2. 遞歸檢測相鄰棋子的相鄰棋子/// 3. 注意:遞歸深度最大為4層(五子棋規則)/// </remarks>List<Chess> GetSameChessByDirection(List<Chess> currentChessTypeList, Chess currentChess, ChessDirection direction){List<Chess> result = new List<Chess>();switch (direction){case ChessDirection.Up: // 正上方檢測(行號+1,列號不變)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row + 1 && item.column == currentChess.column){result.Add(item);// 遞歸檢測更上方的棋子result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.Up));}}break;case ChessDirection.Down: // 正下方檢測(行號-1,列號不變)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row - 1 && item.column == currentChess.column){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.Down));}}break;case ChessDirection.Left: // 正左方檢測(列號-1,行號不變)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row && item.column == currentChess.column - 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.Left));}}break;case ChessDirection.Right: // 正右方檢測(列號+1,行號不變)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row && item.column == currentChess.column + 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.Right));}}break;case ChessDirection.LeftUp: // 左上方檢測(行號+1,列號-1)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row + 1 && item.column == currentChess.column - 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.LeftUp));}}break;case ChessDirection.RightDown: // 右下方檢測(行號-1,列號+1)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row - 1 && item.column == currentChess.column + 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.RightDown));}}break;case ChessDirection.LeftDown: // 左下方檢測(行號-1,列號-1)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row - 1 && item.column == currentChess.column - 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.LeftDown));}}break;case ChessDirection.RightUp: // 右上方檢測(行號+1,列號+1)foreach (Chess item in currentChessTypeList){if (item.row == currentChess.row + 1 && item.column == currentChess.column + 1){result.Add(item);result.AddRange(GetSameChessByDirection(currentChessTypeList, item, ChessDirection.RightUp));}}break;}return result;}
}// 棋子尋找方向
public enum ChessDirection
{Up,Down,Left,Right,LeftUp,RightDown,LeftDown,RightUp
}

NetWorkManager.cs

場景中創建空對象,命名為NetWokManager并添加NetWokManager.cs。

編寫NetWokManager.cs,繼承于MonoBehaviourPunCallbacks

在NetWorkManager中生成玩家:通過RPC的方式設置玩家的棋子類型,能讓兩邊客戶端同步,如果使用 newPlayer.GetComponent<Player>``().chessType = ChessType.Black;這種方式設置,只會在本地設置棋子類型。

比如當前默認玩家棋子類型是黑棋,A創建并進入房間后,將自己設置為黑棋,B進入房間后將自己設置為白棋。

在A客戶端,B依舊是默認的黑棋,因為B修改自身棋子類型只在本地運行,

在B客戶端,A是默認的黑棋,B是設置后的白棋,B客戶端看起來正確只是因為剛好默認的是對的,而A客戶端是錯誤的。

而RPC方式調用,參數RpcTarget.All,是讓所有客戶端都執行 SetChessType方法,所以這里其實主要是后進來的玩家需要通過RPC讓兩邊都執行 SetChessType方法,將后來的玩家設置為白棋。

using System.Collections.Generic;
using UnityEngine;
using Photon.Pun;
using Photon.Realtime;
using TMPro;
using System.Linq;/// <summary>
/// 游戲狀態枚舉(對應游戲流程階段)
/// </summary>
public enum GameState
{Ready = 1,     // 準備階段(玩家未準備)Start = 2,     // 游戲進行階段(雙方已準備)GameOver = 3   // 游戲結束階段(已分出勝負)
}/// <summary>
/// 網絡管理核心類(繼承Photon回調接口)
/// 功能:處理網絡連接、玩家管理、游戲狀態同步
/// </summary>
public class NetWorkManager : MonoBehaviourPunCallbacks
{// 玩家預制體配置public GameObject player;   // 玩家角色預制體(需提前拖拽賦值)// 游戲回合控制public ChessType playerTurn = ChessType.Black;  // 當前回合玩家棋子類型(默認黑方先手)// 游戲狀態管理public GameState gameState = GameState.Ready;   // 當前游戲狀態// UI組件綁定public TextMeshProUGUI readyText;        // 準備按鈕文本組件public TextMeshProUGUI selfChessText;    // 顯示本機玩家棋子類型的文本public TextMeshProUGUI selfReadyText;    // 本機玩家準備狀態文本public TextMeshProUGUI hostileChessText; // 顯示對手棋子類型的文本public TextMeshProUGUI hostileReadyText; // 對手準備狀態文本public TextMeshProUGUI turnText;         // 回合提示文本public TextMeshProUGUI gameOverText;     // 游戲結束提示文本public TextMeshProUGUI winText;          // 勝利者顯示文本// 音效組件public AudioSource markingAudio;    // 落子音效組件// 玩家映射表(維護Photon玩家與本地Player實例的關系)// Key: Photon Player的ActorNumber, Value: 對應的MyGame.Player實例private Dictionary<int, Player> photonPlayerToLocalPlayer = new Dictionary<int, Player>();/// <summary>/// 注冊玩家映射關系,用于當生成玩家對象時記錄映射/// </summary>/// <param name="actorNumber">Photon玩家的唯一標識</param>/// <param name="localPlayer">對應的本地Player實例</param>public void RegisterPlayer(int actorNumber, Player localPlayer){photonPlayerToLocalPlayer[actorNumber] = localPlayer;}void Start(){SetUIState();   // 初始化UI狀態PhotonNetwork.ConnectUsingSettings(); // 連接到Photon云服務}/// <summary>/// 成功連接Photon主服務器回調/// 觸發時機:完成網絡握手并連接到區域服務器/// </summary>public override void OnConnectedToMaster(){base.OnConnectedToMaster();Debug.Log("成功連接至Photon主服務器");// 配置房間參數RoomOptions roomOptions = new RoomOptions();roomOptions.MaxPlayers = 2; // 設置最大玩家數為2(五子棋雙人對戰)// 加入或創建房間(房間名"WuZiQi",類型為默認大廳)PhotonNetwork.JoinOrCreateRoom("WuZiQi", roomOptions, TypedLobby.Default);}/// <summary>/// 成功加入房間回調/// 觸發時機:本地玩家加入指定房間后/// </summary>public override void OnJoinedRoom(){base.OnJoinedRoom();Debug.Log("成功加入房間,當前房間人數:" + PhotonNetwork.CurrentRoom.PlayerCount);// 安全檢查:確保玩家預制體已配置if (player == null){Debug.LogError("玩家預制體未配置!");return;}// 實例化網絡玩家對象(在所有客戶端同步生成)GameObject newPlayer = PhotonNetwork.Instantiate(player.name, Vector3.zero, Quaternion.identity);// 房主設置棋子顏色并同步if (PhotonNetwork.IsMasterClient){// 使用RPC同步黑棋設置(All表示所有客戶端執行)newPlayer.GetComponent<PhotonView>().RPC("SetChessType",RpcTarget.All,ChessType.Black);}else{// 非房主設置白棋newPlayer.GetComponent<PhotonView>().RPC("SetChessType",RpcTarget.All,ChessType.White);}}/// <summary>/// [PunRPC] 切換回合控制權(網絡同步方法)/// </summary>/// <remarks>/// 在所有客戶端同步更新回合狀態和UI提示/// </remarks>[PunRPC]public void ChangeTurn(){// 切換當前回合玩家顏色playerTurn = playerTurn == ChessType.Black ? ChessType.White : ChessType.Black;// 更新回合提示文本turnText.text = playerTurn == ChessType.Black ? "請黑方落子" : "請白方落子";}/// <summary>/// [PunRPC] 游戲結束處理(網絡同步方法)/// </summary>/// <param name="winChessType">勝利方棋子顏色</param>/// <remarks>/// 會在所有客戶端顯示游戲結束界面/// </remarks>[PunRPC]public void GameOver(ChessType winChessType){// 更新游戲狀態gameState = GameState.GameOver;// 安全檢查UI組件引用if (gameOverText){// 顯示游戲結束界面gameOverText.gameObject.SetActive(true);gameOverText.text = "Game Over";// 設置勝利者文本winText.text = winChessType == ChessType.Black ? "黑方獲勝" : "白方獲勝";}}/// <summary>/// [PunRPC] 重置游戲到初始狀態(網絡同步方法)/// </summary>/// <remarks>/// 1. 重置UI狀態/// 2. 重置所有玩家準備狀態/// 3. 恢復默認回合順序/// </remarks>[PunRPC]public void ReSetGame(){// 重置準備相關UIreadyText.text = "準備";selfReadyText.text = "未準備";hostileReadyText.text = "未準備";// 重置所有玩家狀態List<Player> players = GameObject.FindObjectsOfType<Player>().ToList();foreach (Player p in players){p.playerState = PlayerState.NotReady;}// 恢復初始回合設置playerTurn = ChessType.Black;turnText.text = "請黑方落子";}/// <summary>/// [PunRPC] 播放落子音效(網絡同步方法)/// </summary>/// <remarks>/// 所有客戶端同步播放音效/// </remarks>[PunRPC]public void PlayMarkingAudio(){if (markingAudio == null) return;  // 空值保護markingAudio.Play();  // 播放音效文件}/// <summary>/// 準備按鈕點擊事件處理/// </summary>/// <remarks>/// 執行流程:/// 1. 防止重復準備/// 2. 更新本地UI/// 3. 網絡同步準備狀態/// 4. 重置游戲狀態/// </remarks>public void OnClickReadyButton(){// 防止重復點擊if (readyText.text == "已準備") return;// 更新準備按鈕狀態readyText.text = "已準備";// 遍歷所有玩家對象var players = GameObject.FindObjectsOfType<Player>();foreach (Player p in players){// 只同步當前客戶端的玩家狀態if (p.GetComponent<PhotonView>().IsMine){p.GetComponent<PhotonView>().RPC("SetPlayerReady", RpcTarget.All);}}// 更新游戲狀態gameState = GameState.Ready;// 隱藏結束界面gameOverText.gameObject.SetActive(false);// 清理棋盤上所有棋子List<Chess> chessList = GameObject.FindObjectsOfType<Chess>().ToList();foreach (Chess chess in chessList){GameObject.Destroy(chess.gameObject);}}/// <summary>/// 初始化所有UI狀態/// </summary>public void SetUIState(){// 準備相關控件readyText.text = "準備";selfChessText.text = "";selfReadyText.text = "";// 對手信息控件hostileChessText.text = "";hostileReadyText.text = "";// 游戲進程控件turnText.text = "請黑方落子";gameOverText.gameObject.SetActive(false);winText.text = "";}/// <summary>/// 設置本機玩家信息顯示/// </summary>/// <param name="chessType">當前玩家的棋子顏色</param>public void SetSelfText(ChessType chessType){selfChessText.text = chessType == ChessType.Black ? "黑方" : "白方";selfReadyText.text = "未準備";}/// <summary>/// 設置對手玩家信息顯示/// </summary>/// <param name="chessType">對手玩家的棋子顏色</param>public void SetHostilefText(ChessType chessType){hostileChessText.text = chessType == ChessType.Black ? "黑方" : "白方";hostileReadyText.text = "未準備";}/// <summary>/// 當有新玩家加入房間時的回調方法(Photon網絡事件)/// </summary>/// <param name="newPhotonPlayer">新加入的Photon玩家對象</param>/// <remarks>/// 核心功能:房主向新玩家同步現有玩家的狀態/// 執行邏輯:/// 1. 僅由房主執行同步操作/// 2. 遍歷除新玩家外的所有已有玩家/// 3. 通過ActorNumber映射找到對應的本地Player實例/// 4. 向新玩家定向發送同步數據/// </remarks>public override void OnPlayerEnteredRoom(Photon.Realtime.Player newPhotonPlayer){base.OnPlayerEnteredRoom(newPhotonPlayer);if (PhotonNetwork.IsMasterClient){// 遍歷房間內所有Photon玩家(包括自己)foreach (Photon.Realtime.Player photonPlayer in PhotonNetwork.PlayerList){// 跳過新加入的玩家(只需要同步現有玩家狀態)if (photonPlayer != newPhotonPlayer){// 通過Photon的ActorNumber查找對應的游戲內Player對象if (photonPlayerToLocalPlayer.TryGetValue(photonPlayer.ActorNumber, out Player localPlayer)){// 向新玩家定向發送RPC(僅限新玩家接收)photonView.RPC("SyncPlayerState",newPhotonPlayer,          // 指定接收者:新玩家photonPlayer.ActorNumber, // 目標玩家的唯一網絡標識localPlayer.chessType,    // 玩家棋子顏色狀態localPlayer.playerState   // 玩家準備狀態);}}}}}/// <summary>/// [PunRPC] 同步玩家狀態數據(網絡同步方法)/// </summary>/// <param name="targetActorNumber">目標玩家的Photon ActorNumber</param>/// <param name="chessType">需要同步的棋子顏色</param>/// <param name="playerState">需要同步的準備狀態</param>/// <remarks>/// 核心功能:根據網絡同步數據更新本地玩家狀態和UI/// 注意:由于是定向發送,該方法只會在新加入的客戶端執行/// </remarks>[PunRPC]private void SyncPlayerState(int targetActorNumber, ChessType chessType, PlayerState playerState){// 通過ActorNumber查找對應的本地Player實例if (photonPlayerToLocalPlayer.TryGetValue(targetActorNumber, out Player targetPlayer)){// 更新本地玩家眼中,其他玩家的狀態targetPlayer.chessType = chessType;targetPlayer.playerState = playerState;// 更新UI,其實已經排除自己了,targetPlayer一定不會是自己if (targetPlayer.photonView.IsMine){// 更新本機玩家UIselfChessText.text = chessType == ChessType.Black ? "黑方" : "白方";selfReadyText.text = playerState == PlayerState.NotReady ? "未準備" : "已準備";}else{// 更新對手玩家UIhostileChessText.text = chessType == ChessType.Black ? "黑方" : "白方";hostileReadyText.text = playerState == PlayerState.NotReady ? "未準備" : "已準備";}}}/// <summary>/// 當有玩家離開房間時的回調方法(Photon網絡事件)/// </summary>/// <param name="otherPlayer">離開的Photon玩家對象</param>/// <remarks>/// 維護映射關系:及時清理已離開玩家的數據/// 防止后續操作訪問到無效玩家引用/// </remarks>public override void OnPlayerLeftRoom(Photon.Realtime.Player otherPlayer){// 從映射字典中移除離開的玩家photonPlayerToLocalPlayer.Remove(otherPlayer.ActorNumber);}/// <summary>/// 當房主切換時的回調方法(Photon網絡事件)/// </summary>/// <param name="newMaster">新任房主的Photon玩家對象</param>/// <remarks>/// 特殊處理:新房主需要重新同步所有玩家狀態/// 當前實現策略:/// 1. 新房主遍歷本地維護的玩家映射表/// 2. 向其他客戶端同步每個玩家的最新狀態/// 優化建議:可在此處實現換先手邏輯(當前未實現)/// </remarks>public override void OnMasterClientSwitched(Photon.Realtime.Player newMaster){if (PhotonNetwork.IsMasterClient){// 遍歷本地維護的所有玩家映射關系foreach (var kvp in photonPlayerToLocalPlayer){// 向其他客戶端同步玩家狀態photonView.RPC("SyncPlayerState",RpcTarget.Others,    // 發送給除自己外的其他玩家kvp.Key,             // ActorNumberkvp.Value.chessType, // 棋子顏色kvp.Value.playerState// 準備狀態);}}}
}

使用PhotonNetwork.Instantiate生成游戲對象,會在所有客戶端都生成,確保同步。

如果要測試創建情況,將以下內容設置好后,構建并啟動一個程序并且運行Unity編輯器中的場景,觀察Player的創建情況。分別啟動Editor版本+Build版本,可以更好的在Editor中觀察運行情況,更好發現哪里有問題。

在這里插入圖片描述

此時應該有兩個Player(Clone)

在這里插入圖片描述

踩坑經驗總結

問題現象
  1. 每次點擊會生成兩個棋子
  2. 棋子位置固定,不跟隨鼠標點擊位置
  3. 玩家準備狀態不同步

原因分析

  1. 多Player實例沖突

    • 場景中預先放置了一個靜態Player對象,而Photon網絡又動態生成了一個Player。
    • 兩個Player的腳本同時運行,導致每次點擊觸發兩次棋子生成。
  2. 相機投影模式錯誤

    • Unity默認的3D項目使用 透視模式(Perspective) ,導致 ScreenToWorldPoint坐標轉換時Z軸計算錯誤。
    • 棋子實際生成在遠離棋盤的3D空間中(比如Z軸位置不對),看起來像位置固定。
  3. 準備狀態僅在本地生效,其他客戶端無法感知

    • 新玩家加入時未獲取房間內已有玩家的準備狀態
    • 房主準備并同步信息時新玩家并不存在,新玩家到來時,房主的同步行為已經結束,新玩家看到的是默認狀態

解決方案

1. 網絡對象管理:一人一角色
  • 操作:
    • 刪除場景中預先放置的Player對象,其實是忘記刪了。嘿嘿
    • 改為 完全通過Photon動態生成Player (在代碼中調用 PhotonNetwork.Instantiate)。
2. 相機設置:切換正交模式
  • 操作:
    • 將相機 ProjectionPerspective(透視)改為 Orthographic(正交)。
    • 調整相機 Size值,確保整個棋盤可見。
  • 原理:
    正交模式忽略Z軸深度,鼠標坐標轉換更簡單直觀(類似2D游戲)。
3.使用字典將玩家實例(Photon.Realtime.Player)的ActorNumber與Player腳本對應映射,存儲準備狀態用于同步
  • // 玩家映射表(維護Photon玩家與本地Player實例的關系)
    // Key: Photon Player的ActorNumber, Value: 對應的MyGame.Player實例
    private Dictionary<int, Player> photonPlayerToLocalPlayer = new Dictionary<int, Player>();
    
  • OnPlayerEnteredRoom 中由房主遍歷并同步所有玩家狀態
  • // 向新玩家定向發送RPC(僅限新玩家接收)
    photonView.RPC("SyncPlayerState",newPhotonPlayer,          // 指定接收者:新玩家photonPlayer.ActorNumber, // 目標玩家的唯一網絡標識localPlayer.chessType,    // 玩家棋子顏色狀態localPlayer.playerState   // 玩家準備狀態
    );
    

參考鏈接

從零手寫Unity3D經典游戲五子棋開發+Photon網絡聯機游戲_嗶哩嗶哩_bilibili

倉庫地址

MapleInori/WuZiQi: 學習記錄 (github.com)


寫文檔真難啊,有點寫不明白。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/75709.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/75709.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/75709.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

(UI自動化測試web端)第二篇:元素定位的方法_css定位之屬性選擇器

看代碼里的【find_element_by_css_selector( )】( )里的表達式怎么寫&#xff1f; 文章介紹了第四種寫法屬性選擇器 &#xff0c;你要根據網頁中的實際情況來判斷自己到底要用哪一種方法來進行元素定位。每種方法都要多練習&#xff0c;全都熟了之后你在工作當中使用起來元素定…

預編譯能否 100%防 sql 注入?

&#x1f31f; 什么是 SQL 注入&#xff1f; SQL 注入&#xff08;SQL Injection&#xff09;是指攻擊者利用特殊輸入&#xff0c;讓數據庫執行它本來不應該執行的代碼&#xff0c;從而獲取或篡改數據。 就像在考試的時候偷偷改題目&#xff0c;讓老師改成你想要的內容&#…

第十五章 | Layer2、Rollup 與 ZK 技術實戰解析

&#x1f4da; 第十五章 | Layer2、Rollup 與 ZK 技術實戰解析 ——構建下一代高性能區塊鏈應用&#xff0c;從 Solidity 到 zkSync&#xff01; ? 本章導讀 Layer2 和零知識證明&#xff08;ZK&#xff09;正成為區塊鏈發展的核心方向。 隨著主網 Gas 居高不下、TPS 無法滿…

2025-03-26 學習記錄--C/C++-PTA 6-3 求鏈式表的表長

合抱之木&#xff0c;生于毫末&#xff1b;九層之臺&#xff0c;起于累土&#xff1b;千里之行&#xff0c;始于足下。&#x1f4aa;&#x1f3fb; 一、題目描述 ?? 6-3 求鏈式表的表長 本題要求實現一個函數&#xff0c;求鏈式表的表長。 函數接口定義&#xff1a; &…

【Linux】Linux_Ubuntu與Windows之間的文件傳輸

一、Linux終端命令的復制粘貼 1.打開linux 終端&#xff0c;輸入以下命令&#xff1a;&#xff08;注意&#xff0c;需要聯網&#xff09; 2.命令行下載&#xff1a; sudo apt-get autoremove open-vm-tools 3.命令行安裝&#xff1a; sudo apt-get install open-vm-tools-…

Python Sanic面試題及參考答案

目錄 Sanic 的事件循環機制與 uvloop 的關系 Sanic 的 Request/Response 對象生命周期如何管理?如何訪問請求上下文? 對比 Sanic 與 Flask/Django 的異步處理模型差異 Sanic 的 Blueprint 機制如何實現模塊化路由?如何處理跨藍圖中間件? 如何在 Sanic 中實現 WebSocket…

算法每日一練 (18)

&#x1f4a2;歡迎來到張翊塵的技術站 &#x1f4a5;技術如江河&#xff0c;匯聚眾志成。代碼似星辰&#xff0c;照亮行征程。開源精神長&#xff0c;傳承永不忘。攜手共前行&#xff0c;未來更輝煌&#x1f4a5; 文章目錄 算法每日一練 (18)刪除并獲得點數題目描述解題思路解題…

VsCode啟用右括號自動跳過(自動重寫) - 自錄制gif演示

VsCode啟用右括號自動跳過(自動重寫) - 自錄制gif演示 前言 不知道大家在編程時候的按鍵習慣是怎樣的。輸入完左括號后編輯器一般會自動補全右括號&#xff0c;輸入完左括號的內容后&#xff0c;是按→跳過右括號還是按)跳過右括號呢&#xff1f; for (int i 0; i < a.s…

用Python和Stable Diffusion生成AI動畫:從圖像到視頻的全流程指南

引言 本文將演示如何通過Python代碼實現基于文本提示的AI動畫生成。我們將使用Stable Diffusion生成連貫圖像幀,結合OpenCV合成視頻,最終實現一個可自定義的動畫生成 pipeline。 一、環境準備 1. 依賴安裝 # 安裝核心庫 pip install diffusers transformers torch numpy …

【Git 常用指令速查表】

Git 常用指令速查表 Git 常用指令速查表目錄1. 初始化倉庫2. 提交代碼流程3. 分支管理4. 遠程倉庫操作5. 撤銷操作6. 查看狀態與日志7. 其他實用指令完整操作示例常用場景速查表 Git 常用指令速查表 目錄 初始化倉庫提交代碼流程分支管理遠程倉庫操作撤銷操作查看狀態與日志其…

分布式爬蟲框架Scrapy-Redis實戰指南

引言 在當今數字化的時代背景下&#xff0c;互聯網技術的蓬勃興起極大地改變了旅游酒店業的運營模式與市場格局。作為旅游產業鏈中的關鍵一環&#xff0c;酒店業的興衰與互聯網技術的應用程度緊密相連。分布式爬蟲技術&#xff0c;尤其是基于 Scrapy 框架的 Scrapy-Redis 擴展…

爬蟲:scrapy面試題大全(60個scrapy經典面試題和詳解)

更多內容請見: 爬蟲和逆向教程-專欄介紹和目錄 文章目錄 1. 什么是Scrapy?2. Scrapy 框架的組件及其作用?3. Scrapy的工作流程是什么?(運行機制)4. 如何創建一個Scrapy項目?5. 如何定義一個Spider?6. 如何在Scrapy中提取數據?7. Scrapy中的Item是什么?8. Scrapy中的P…

Leetcode12-整數轉羅馬數字

題目鏈接&#xff1a;12. 整數轉羅馬數字 - 力扣&#xff08;LeetCode&#xff09; 看題目限制輸入1 < num < 3999&#xff0c;就直接用暴力法寫了&#xff0c;還比較簡單 代碼&#xff1a; char* intToRoman(int num) {char *res (char*)malloc(100);int index 0;i…

WebMvcConfigurer 的 addResourceLocations

在 Spring Boot 的 addResourceLocations 方法中&#xff0c;file: 是一個 URL 前綴&#xff0c;用于指示資源的位置是本地文件系統路徑。以下是詳細解釋&#xff1a; 一、file: 的作用 file: 是 Java 中用于表示本地文件系統的 URL 前綴。它告訴 Spring Boot&#xff0c;資源…

Spring Boot響應壓縮配置與優化

一、核心工作機制 1.1 自動協商觸發條件 Spring Boot的響應壓縮功能基于智能協商機制&#xff0c;需同時滿足以下條件方可觸發&#xff1a; 客戶端支持&#xff1a;請求頭包含Accept-Encoding: gzip/deflate數據量閾值&#xff1a;響應體大小超過預設值&#xff08;默認2KB&…

JavaScript 改變 HTML 樣式

JavaScript 改變 HTML 樣式 JavaScript 改變 HTML 樣式的核心是通過操作 DOM 元素的 CSS 屬性或 類名 實現動態視覺效果。以下是具體方法與場景解析: 一、直接修改元素的 style 屬性 通過 DOM 元素的 style 屬性直接設置內聯樣式,優先級最高: // 修改單個樣式 document.…

【vue】vue + vant實現上傳圖片添加水印

目錄 方法1&#xff1a;使用HTML2canvas 說明&#xff1a; 優點 缺點 依賴安裝 方法2&#xff1a;使用canvas結合vant中組件 增加水印方法 在vue組件中使用 要點 方法1&#xff1a;使用HTML2canvas 使用html2canvas來處理水印的生成&#xff0c;需要就給水印元素轉換為…

【深度破解】爬蟲反反爬核心技術實踐:驗證碼識別與指紋偽裝

一、反爬技術體系全景圖 現代Web應用的常見反爬手段&#xff1a; mermaid&#xff1a; graph TDA[反爬體系] --> B[行為特征檢測]A --> C[驗證碼體系]A --> D[指紋追蹤]B --> B1[請求頻率]B --> B2[鼠標軌跡]B --> B3[頁面停留時間]C --> C1[圖形驗證碼…

deepseek(2)——deepseek 關鍵技術

1 Multi-Head Latent Attention (MLA) MLA的核心在于通過低秩聯合壓縮來減少注意力鍵&#xff08;keys&#xff09;和值&#xff08;values&#xff09;在推理過程中的緩存&#xff0c;從而提高推理效率&#xff1a; c t K V W D K V h t c_t^{KV} W^{DKV}h_t ctKV?WDKVht?…

OpenGL繪制文本

一&#xff1a;QPainter繪制 在 OpenGL 渲染的窗口中&#xff08;如 QOpenGLWidget&#xff09;&#xff0c;通過 QPainter 直接繪制文本。Qt 會自動將 2D 內容&#xff08;文本、圖形&#xff09;與 OpenGL 內容合成。在paintGL()里面繪制&#xff0c;如果有其他紋理&#xf…