// If we've already stored a device identifier in PlayerPrefs then use that.if(PlayerPrefs.HasKey(DeviceIdentifierPrefName)){deviceId=PlayerPrefs.GetString(DeviceIdentifierPrefName);}
if(deviceId==SystemInfo.unsupportedIdentifier){deviceId=System.Guid.NewGuid().ToString();}// Store the device identifier to ensure we use the same one each time from now on.PlayerPrefs.SetString(DeviceIdentifierPrefName,deviceId);
我们现在与 Nakama 服务器进行身份验证,将身份验证令牌存储到 PlayerPrefs 中:
NakamaConnection.cs
1
2
3
4
5
// Use Nakama Device authentication to create a new session using the device identifier.Session=awaitClient.AuthenticateDeviceAsync(deviceId);// Store the auth token that comes back so that we can restore the session later if necessary.PlayerPrefs.SetString(SessionPrefName,Session.AuthToken);
现在我们有一个有效的 Nakama 会话,我们继续并打开 Socket:
NakamaConnection.cs
1
2
3
// Open a new Socket for real-time communication.Socket=Client.NewSocket(true);awaitSocket.ConnectAsync(Session,true);
这样我们就可以开始与 Nakama 服务器以及其他连接到我们游戏的玩家进行通信。在 Fish Game 中调用此连接是通过 GameManager 对象完成:
为回顾在 Fish Game 中匹配如何工作,我们深入探讨 GameManager、NakamaConnection 和 MainMenu 类,并讨论相关函数。参阅匹配程序文档,详细了解 Nakama 中的匹配。
您在此创建新的 Dictionary 来存储连接的玩家并订阅以下事件:
ReceivedMatchmakerMatched:让您能在 Nakama 找到匹配时做出反应。
ReceivedMatchPresence:让您能在玩家加入或退出比赛时做出反应。
ReceivedMatchState:让您能在从 Nakama 服务器接收到消息时做出反应。
GameManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// <summary>/// Called by Unity when the GameObject starts./// </summary>privateasyncvoidStart(){// Create an empty dictionary to hold references to the currently connected players.players=newDictionary<string,GameObject>();// ...// Setup network event handlers.NakamaConnection.Socket.ReceivedMatchmakerMatched+=OnReceivedMatchmakerMatched;NakamaConnection.Socket.ReceivedMatchPresence+=OnReceivedMatchPresence;NakamaConnection.Socket.ReceivedMatchState+=asyncm=>awaitOnReceivedMatchState(m);// ...}
/// <summary>/// Starts looking for a match with a given number of minimum players./// </summary>publicasyncTaskFindMatch(intminPlayers=2){// Set some matchmaking properties to ensure we only look for games that are using the Unity client.// This is not a required when using the Unity Nakama SDK,// however in this instance we are using it to differentiate different matchmaking requests across multiple platforms using the same Nakama server.varmatchmakingProperties=newDictionary<string,string>{{"engine","unity"}};// Add this client to the matchmaking pool and get a ticket.varmatchmakerTicket=awaitSocket.AddMatchmakerAsync("+properties.engine:unity",minPlayers,minPlayers,matchmakingProperties);currentMatchmakingTicket=matchmakerTicket.Ticket;}
/// <summary>/// Cancels the current matchmaking request./// </summary>publicasyncTaskCancelMatchmaking(){awaitSocket.RemoveMatchmakerAsync(currentMatchmakingTicket);}
/// <summary>/// Called when a MatchmakerMatched event is received from the Nakama server./// </summary>/// <param name="matched">The MatchmakerMatched data.</param>privateasyncvoidOnReceivedMatchmakerMatched(IMatchmakerMatchedmatched){// Cache a reference to the local user.localUser=matched.Self.Presence;// Join the match.varmatch=awaitNakamaConnection.Socket.JoinMatchAsync(matched);// ...// Spawn a player instance for each connected user.foreach(varuserinmatch.Presences){SpawnPlayer(match.Id,user);}// Cache a reference to the current match.currentMatch=match;}
存储的对 Self.Presence 值的引用让您可以快速方便地访问本地玩家的 Nakama 状态,对于获取他们的会话 ID、用户 ID 和用户名等信息非常实用。
存储的对 JoinMatchAsync() 返回值的引用能让您访问当前匹配 ID 和连接的用户等信息
/// <summary>/// Called when a player/s joins or leaves the match./// </summary>/// <param name="matchPresenceEvent">The MatchPresenceEvent data.</param>privatevoidOnReceivedMatchPresence(IMatchPresenceEventmatchPresenceEvent){// For each new user that joins, spawn a player for them.foreach(varuserinmatchPresenceEvent.Joins){SpawnPlayer(matchPresenceEvent.MatchId,user);}// For each player that leaves, despawn their player.foreach(varuserinmatchPresenceEvent.Leaves){if(players.ContainsKey(user.SessionId)){Destroy(players[user.SessionId]);players.Remove(user.SessionId);}}}
// If the spawnIndex is -1 then pick a spawn point at random, otherwise spawn the player at the specified spawn point.varspawnPoint=spawnIndex==-1?SpawnPoints.transform.GetChild(Random.Range(0,SpawnPoints.transform.childCount-1)):SpawnPoints.transform.GetChild(spawnIndex);
// Set a variable to check if the player is the local player or not based on session ID.varisLocal=user.SessionId==localUser.SessionId;// Choose the appropriate player prefab based on if it's the local player or not.varplayerPrefab=isLocal?NetworkLocalPlayerPrefab:NetworkRemotePlayerPrefab;// Spawn the new player.varplayer=Instantiate(playerPrefab,spawnPoint.transform.position,Quaternion.identity);
接下来查看玩家是否远程连接,如是,则抓取对 PlayerNetworkRemoteSync 组件的引用,设置合适的网络数据。我们将当前比赛 ID 传递给它,并引用此用户的 IUserPresence 对象:
GameManager.cs
1
2
3
4
5
6
7
8
9
// Setup the appropriate network data values if this is a remote player.if(!isLocal){player.GetComponent<PlayerNetworkRemoteSync>().NetworkData=newRemotePlayerNetworkData{MatchId=matchId,User=user};}
这用于在接收到数据时,核实此数据是否预定传送给该具体玩家。
然后我们将玩家会话 ID 用作密钥,将其添加到 players 字典,并将玩家 GameObject 作为此值传入:
GameManager.cs
1
2
// Add the player to the players array.players.Add(user.SessionId,player);
接下来检查此玩家是否在本地,如是,则设置其 PlayerDied 事件的事件侦听器:
GameManager.cs
1
2
3
4
5
6
// If this is our local player, add a listener for the PlayerDied event.if(isLocal){localPlayer=player;player.GetComponent<PlayerHealthController>().PlayerDied.AddListener(OnLocalPlayerDied);}
// Send a network packet containing the player's velocity and position.gameManager.SendMatchState(OpCodes.VelocityAndPosition,MatchDataJson.VelocityAndPosition(playerRigidbody.velocity,playerTransform.position));
接下来,我们检查本地玩家的输入自上次运行此函数以来是否发生了变化,如果没有,则提前返回:
PlayerNetworkLocalSync.cs
1
2
3
4
5
// If the players input hasn't changed, return early.if(!playerInputController.InputChanged){return;}
最后,我们向整个网络发送玩家的当前输入:
PlayerNetworkLocalSync.cs
1
2
3
4
5
// Send network packet with the player's current input.gameManager.SendMatchState(OpCodes.Input,MatchDataJson.Input(playerInputController.HorizontalInput,playerInputController.Jump,playerInputController.JumpHeld,playerInputController.Attack));
为回顾如何从 NetworkRemotePlayer 从全网络接收位置和输入更新,请查看 Entities > Player > PlayerNetworkRemoteSync.cs。
首先,我们有两个公共变量:一个存储对当前比赛 ID 和玩家的 IUserPresence 对象的引用,另一个确定我们插入玩家位置的速度:
PlayerNetworkRemoteSync.cs
1
2
3
4
5
6
publicRemotePlayerNetworkDataNetworkData;/// <summary>/// The speed (in seconds) in which to smoothly interpolate to the player's actual position when receiving corrected data./// </summary>publicfloatLerpTime=0.05f;
// Add an event listener to handle incoming match state data.gameManager.NakamaConnection.Socket.ReceivedMatchState+=OnReceivedMatchState;// .../// <summary>/// Called when receiving match data from the Nakama server./// </summary>/// <param name="matchState">The incoming match state data.</param>privatevoidOnReceivedMatchState(IMatchStatematchState){// If the incoming data is not related to this remote player, ignore it and return early.if(matchState.UserPresence.SessionId!=NetworkData.User.SessionId){return;}// Decide what to do based on the Operation Code of the incoming state data as defined in OpCodes.switch(matchState.OpCode){caseOpCodes.VelocityAndPosition:UpdateVelocityAndPositionFromState(matchState.State);break;caseOpCodes.Input:SetInputFromState(matchState.State);break;caseOpCodes.Died:playerMovementController.PlayDeathAnimation();break;default:break;}}
/// <summary>/// Updates the player's velocity and position based on incoming state data./// </summary>/// <param name="state">The incoming state byte array.</param>privatevoidUpdateVelocityAndPositionFromState(byte[]state){varstateDictionary=GetStateAsDictionary(state);playerRigidbody.velocity=newVector2(float.Parse(stateDictionary["velocity.x"]),float.Parse(stateDictionary["velocity.y"]));varposition=newVector3(float.Parse(stateDictionary["position.x"]),float.Parse(stateDictionary["position.y"]),0);// Begin lerping to the corrected position.lerpFromPosition=playerTransform.position;lerpToPosition=position;lerpTimer=0;lerpPosition=true;}
/// <summary>/// Sets the appropriate input values on the PlayerMovementController and PlayerWeaponController based on incoming state data./// </summary>/// <param name="state">The incoming state Dictionary.</param>privatevoidSetInputFromState(byte[]state){varstateDictionary=GetStateAsDictionary(state);playerMovementController.SetHorizontalMovement(float.Parse(stateDictionary["horizontalInput"]));playerMovementController.SetJump(bool.Parse(stateDictionary["jump"]));playerMovementController.SetJumpHeld(bool.Parse(stateDictionary["jumpHeld"]));if(bool.Parse(stateDictionary["attack"])){playerWeaponController.Attack();}}
/// <summary>/// Called when this GameObject is being destroyed./// </summary>privatevoidOnDestroy(){if(gameManager!=null){gameManager.NakamaConnection.Socket.ReceivedMatchState-=OnReceivedMatchState;}}
publicPlayerDiedEventPlayerDied;// .../// <summary>/// Reduces the players health by damage, triggers the PlayerDied event if health is 0 or below./// </summary>/// <param name="damage">The amount of damage the player should receive.</param>publicvoidTakeDamage(intdamage=1){// ...// If health falls to 0 or below, disable player input controls, play the death animation and fire the PlayerDied event.if(health<=0){// ...PlayerDied.Invoke(gameObject);}}
本地玩家的死亡通过 GameManager 类中的 OnLocalPlayerDied 函数处理。
GameManager.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
/// <summary>/// Called when the local player dies./// </summary>/// <param name="player">The local player.</param>privateasyncvoidOnLocalPlayerDied(GameObjectplayer){// Send a network message telling everyone that we died.awaitSendMatchStateAsync(OpCodes.Died,MatchDataJson.Died(player.transform.position));// Remove ourself from the players array and destroy our GameObject after 0.5 seconds.players.Remove(localUser.SessionId);Destroy(player,0.5f);}
/// <summary>/// Called when new match state is received./// </summary>/// <param name="matchState">The MatchState data.</param>privateasyncTaskOnReceivedMatchState(IMatchStatematchState){// Get the local user's session ID.varuserSessionId=matchState.UserPresence.SessionId;// If the matchState object has any state length, decode it as a Dictionary.varstate=matchState.State.Length>0?System.Text.Encoding.UTF8.GetString(matchState.State).FromJson<Dictionary<string,string>>():null;// Decide what to do based on the Operation Code as defined in OpCodes.switch(matchState.OpCode){caseOpCodes.Died:// Get a reference to the player who died and destroy their GameObject after 0.5 seconds and remove them from our players array.varplayerToDestroy=players[userSessionId];Destroy(playerToDestroy,0.5f);players.Remove(userSessionId);// If there is only one player left and that us, announce the winner and start a new round.if(players.Count==1&&players.First().Key==localUser.SessionId){AnnounceWinnerAndStartNewRound();}break;caseOpCodes.Respawned:// Spawn the player at the chosen spawn index.SpawnPlayer(currentMatch.Id,matchState.UserPresence,int.Parse(state["spawnIndex"]));break;caseOpCodes.NewRound:// Display the winning player's name and begin a new round.awaitAnnounceWinnerAndRespawn(state["winningPlayerName"]);break;default:break;}}
此处我们获得发送消息的连接用户的会话 ID 引用,将其存储在 userSessionId 中,然后检查 matchState.State.Length 属性,查看网络消息内是否有任何状态数据。
/// <summary>/// Sends a network message that indicates a player has won and a new round is being started./// </summary>/// <returns></returns>publicasyncvoidAnnounceWinnerAndStartNewRound(){// ...varwinningPlayerName=localDisplayName;// Send a network message telling everyone else that we won.awaitSendMatchStateAsync(OpCodes.NewRound,MatchDataJson.StartNewRound(winningPlayerName));// Display the winning player message and respawn our player.awaitAnnounceWinnerAndRespawn(winningPlayerName);}
/// <summary>/// Displays the winning player message and respawns the player./// </summary>/// <param name="winningPlayerName">The name of the winning player.</param>privateasyncTaskAnnounceWinnerAndRespawn(stringwinningPlayerName){// Set the winning player text label.WinningPlayerText.text=string.Format("{0} won this round!",winningPlayerName);// Wait for 2 seconds.awaitTask.Delay(2000);// Reset the winner player text label.WinningPlayerText.text="";// Remove ourself from the players array and destroy our player.players.Remove(localUser.SessionId);Destroy(localPlayer);// Choose a new spawn position and spawn our local player.varspawnIndex=Random.Range(0,SpawnPoints.transform.childCount-1);SpawnPlayer(currentMatch.Id,localUser,spawnIndex);// Tell everyone where we respawned.SendMatchState(OpCodes.Respawned,MatchDataJson.Respawned(spawnIndex));}