Unity3d:多人在线VR游戏实战序章-PUN回合制游戏案例详解
提纲:
Ø IPunTurnManagerCallbacks回调函数...................................................... 4
Ø TurnManager Extensions |TurnManager扩展........................................ 4
PhotonUnity Networking (PUN) 可用于回合制游戏,如策略游戏或棋类游戏。本文档给出了一个如何实现这样的游戏的概述,基于我们的Rock Paper Scissors Demo [1],该案例在PUN包中。
图 1 石头剪刀布游戏预览
演示围绕几个组件脚本:PunTurnManager
, RpsCore
和RpsDemoConnect
。
[1] Rock Paper Scissors Demo,剪刀石头布案例,后面的教程中将其首字母缩写为Rps
0x00 前言
关于PUN的详细教学,请参考我之前发布的Photon多人游戏开发教程 和 Unity3D 多人在线游戏综合开发文档(Photon Network),这一篇主要针对回合制游戏,在官方教程的基础上进行详细的讲解。之所以首选回合制游戏解析,主要是为了接下来要做的中国象棋VR游戏做一些铺垫,该游戏从某一方面说也是回合制的,同时也是RTS策略游戏。
PunTurnManager
是一个通用的组件类,可以被重用于类似的游戏和逻辑。重要的是要认识到它不是强制性的,而是一个简单而漂亮的包装,让你快速入门并运行回合游戏。这当然是构建更复杂和原创的游戏的一个很好的起点。
在数据存储方面,它只依赖于房间自定义属性(Room Custom Properties),这是一个保持游戏数据在游戏生命周期中的非常有效的方法。因此,如果您需要额外的功能和数据,强烈建议您继续对房间自定义属性进行扩展,来定义、读取和写入游戏相关的数据。别忘了你可以用Webhooks来为第三方系统捕捉房间自定义属性(但不只是!)并处理它们的数据,你可以用这个来保持高分、发送通知、维护统计,没有外部数据存储,而只是通过实现Webhooks。下面是详细的代码解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 | using System; using System.Collections.Generic; using ExitGames.Client.Photon; using Photon; using UnityEngine; using ExitGames = ExitGames.Client.Photon.Hashtable; ///
/// Pun回合制游戏管家. /// 为玩家之间典型的回合流程和逻辑提供一个接口(IPunTurnManagerCallbacks) /// 为PhotonPlayer、Room和RoomInfo提供了扩展来满足回合制游戏需求而专门制作的API /// public class PunTurnManager : PunBehaviour { ///
/// 包装了对房间的自定义属性"turn"的访问. /// /// 回合的索引 public int Turn //回合 { get { return PhotonNetwork.room.GetTurn(); } private set { _isOverCallProcessed = false ; PhotonNetwork.room.SetTurn(value, true ); } } ///
/// 回合的持续时间(单位:秒). /// public float TurnDuration = 20f; ///
/// 获取当前回合过去的时间(秒) /// /// 回合流逝的时间. public float ElapsedTimeInTurn { get { return (( float )(PhotonNetwork.ServerTimestamp - PhotonNetwork.room.GetTurnStart()))/1000.0f; } } ///
/// 获取当前回合剩余的时间. 范围从0到TurnDuration /// /// 当前回合剩余的时间(秒) public float RemainingSecondsInTurn { get { return Mathf.Max(0f, this .TurnDuration - this .ElapsedTimeInTurn); } } ///
/// 获取表明回合是否被全部玩家完成的布尔值. /// /// true 如果该回合被所有玩家完成则返回真; 否则返回假, false. public bool IsCompletedByAll { get { return PhotonNetwork.room != null && Turn > 0 && this .finishedPlayers.Count == PhotonNetwork.room.PlayerCount; } } ///
/// 获取表明当前回合是否被我完成的布尔值. /// /// true 如果当前回合被我完成返回真; 否则返回假, false. public bool IsFinishedByMe { get { return this .finishedPlayers.Contains(PhotonNetwork.player); } } ///
/// 获取表明当前回合是否完成的布尔值.即是ElapsedTimeinTurn>=TurnDuration 或 RemainingSecondsInTurn <= 0f /// /// true 如果当前回合完了返回真; 否则返回假, false. public bool IsOver { get { return this .RemainingSecondsInTurn <= 0f; } } ///
/// 回合管家监听器. 设置该监听器到你自己的脚本实例来捕捉回调函数 /// public IPunTurnManagerCallbacks TurnManagerListener; ///
/// 完成回合的玩家哈希集. /// private readonly HashSet finishedPlayers = new HashSet(); ///
/// 回合管家事件偏移事件消息字节. 内部用于定义房间自定义属性中的数据 /// public const byte TurnManagerEventOffset = 0; ///
/// 移动事件消息字节. 内部用于保存房间自定义属性中的数据 /// public const byte EvMove = 1 + TurnManagerEventOffset; ///
/// 最终移动事件消息字节. 内部用于保存房间自定义属性中的数据 /// public const byte EvFinalMove = 2 + TurnManagerEventOffset; // 追踪消息调用 private bool _isOverCallProcessed = false ; #region MonoBehaviour CallBack ///
/// 注册来自PhotonNetwork的事件调用. /// void Start() { PhotonNetwork.OnEventCall = OnEvent; } void Update() { if (Turn > 0 && this .IsOver && !_isOverCallProcessed) { _isOverCallProcessed = true ; this .TurnManagerListener.OnTurnTimeEnds( this .Turn); } } #endregion #region Public Methods ///
/// 告诉TurnManager开始一个新的回合. /// public void BeginTurn() { Turn = this .Turn + 1; // 注意: 这将设置房间里的一个属性,该属性对于其他玩家可用. } ///
/// 调用来发送一个动作. 也可以选择结束该回合. /// 移动对象可以是任何事物. 尝试去优化,只发送严格的最小化信息集来定义回合移动. /// /// 回合移动 /// 是否完成 public void SendMove( object move, bool finished) { if (IsFinishedByMe) { UnityEngine.Debug.LogWarning( "不能SendMove. 该玩家已经完成了这回合." ); return ; } // 与实际移动一起,我们不得不发送该移动属于哪一个回合 Hashtable moveHt = new Hashtable(); moveHt.Add( "turn" , Turn); moveHt.Add( "move" , move); byte evCode = (finished) ? EvFinalMove : EvMove; PhotonNetwork.RaiseEvent(evCode, moveHt, true , new RaiseEventOptions() { CachingOption = EventCaching.AddToRoomCache }); if (finished) { PhotonNetwork.player.SetFinishedTurn(Turn); } // 服务器不会把该事件发送回源头 (默认). 要获取该事件,本地调用即可 // (注意: 事件的顺序可能会混淆,因为我们在本地做这个调用) OnEvent(evCode, moveHt, PhotonNetwork.player.ID); } ///
/// 获取该玩家是否完成了当前回合. /// /// true, 如果传入的玩家完成了当前回合则返回真, false 否则返回假. /// The Player to check for public bool GetPlayerFinishedTurn(PhotonPlayer player) { if (player != null && this .finishedPlayers != null && this .finishedPlayers.Contains(player)) { return true ; } return false ; } #endregion #region Callbacks ///
/// 被PhotonNetwork.OnEventCall的注册调用(Start方法中注册了该事件) /// /// 事件代码. /// 内容. /// 发送者Id. public void OnEvent( byte eventCode, object content, int senderId) { PhotonPlayer sender = PhotonPlayer.Find(senderId); switch (eventCode) { case EvMove: { Hashtable evTable = content as Hashtable; int turn = ( int )evTable[ "turn" ]; object move = evTable[ "move" ]; this .TurnManagerListener.OnPlayerMove(sender, turn, move); break ; } case EvFinalMove: { Hashtable evTable = content as Hashtable; int turn = ( int )evTable[ "turn" ]; object move = evTable[ "move" ]; if (turn == this .Turn) { this .finishedPlayers.Add(sender); this .TurnManagerListener.OnPlayerFinished(sender, turn, move); } if (IsCompletedByAll) { this .TurnManagerListener.OnTurnCompleted( this .Turn); } break ; } } } ///
/// 当一个房间的自定义属性更改时被调用。propertiesThatChanged改变的属性包含所有通过Room.SetCustomProperties设置的. /// /// Properties that changed. public override void OnPhotonCustomRoomPropertiesChanged(Hashtable propertiesThatChanged) { // Debug.Log("OnPhotonCustomRoomPropertiesChanged: "+propertiesThatChanged.ToStringFull()); if (propertiesThatChanged.ContainsKey( "Turn" )) { _isOverCallProcessed = false ; this .finishedPlayers.Clear(); this .TurnManagerListener.OnTurnBegins( this .Turn); } } #endregion }
|
使用IPunTurnManagerCallbacks
接口,你可以监听主要的TurnBased 消息回调。参考RpsCore
脚本来查看PunTurnManager
可以如何被绑定。
适当地监听这些回调函数来跟踪所有客户端的当前情况对于回合管理是至关重要的方面。你可以/应该依赖于这个来检测游戏结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | public interface IPunTurnManagerCallbacks { ///
/// 发起回合开始事件. /// /// 回合. void OnTurnBegins( int turn); ///
/// 当回合完成时调用(被所有玩家完成) /// /// 回合索引 void OnTurnCompleted( int turn); ///
/// 当玩家移动时调用(但是没有完成该回合) /// /// 玩家引用 /// 回合索引 /// 移动对象数据 void OnPlayerMove(PhotonPlayer player, int turn, object move); ///
/// 当玩家完成回合时调用(包括该玩家的动作/移动) /// /// 玩家引用 /// 回合索引 /// 移动对象数据 void OnPlayerFinished(PhotonPlayer player, int turn, object move); ///
/// 当回合由于时间限制完成时调用(回合超时) /// /// 回合索引 void OnTurnTimeEnds( int turn); } |
你会发现PUN中的各种已知类的新属性和方法的扩展,都是专门设计用于回合系统。这些扩展是在TurnExtensions
类中PunTurnManager.cs中被声明的。这提供了一个干净的API,使代码更易读。
尽可能地创建自己的扩展来解决特定的需求。特别是从您的游戏逻辑中隐藏使用自定义属性的玩家和房间。这样做,你允许随着项目变得越来越复杂,随着时间的推移,更灵活和更容易重构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | public static class TurnExtensions { ///
/// 当前进行的回合数 /// public static readonly string TurnPropKey = "Turn" ; ///
/// 当前进行的回合开始(服务器)时间(用于计算结束) /// public static readonly string TurnStartPropKey = "TStart" ; ///
/// 完成回合的演员 (后面接数字) /// public static readonly string FinishedTurnPropKey = "FToA" ; ///
/// 设置该回合. /// /// 房间引用 /// 回合索引 /// 如果设置为真 true 则设置开始时间. public static void SetTurn( this Room room, int turn, bool setStartTime = false ) { if (room == null || room.CustomProperties == null ) { return ; } Hashtable turnProps = new Hashtable(); turnProps[TurnPropKey] = turn; if (setStartTime) { turnProps[TurnStartPropKey] = PhotonNetwork.ServerTimestamp; } room.SetCustomProperties(turnProps); } ///
/// 从RoomInfo获取当前回合 /// /// 返回回合索引 /// RoomInfo引用 public static int GetTurn( this RoomInfo room) { if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnPropKey)) { return 0; } return ( int )room.CustomProperties[TurnPropKey]; } ///
/// 返回回合开始的时间. 可用于计算回合进行的时间. /// /// 返回回合开始时间. /// 房间信息. public static int GetTurnStart( this RoomInfo room) { if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnStartPropKey)) { return 0; } return ( int )room.CustomProperties[TurnStartPropKey]; } ///
/// 获取玩家完成的回合 (从房间属性中) /// /// 返回已完成的回合索引 /// 玩家引用 public static int GetFinishedTurn( this PhotonPlayer player) { Room room = PhotonNetwork.room; if (room == null || room.CustomProperties == null || !room.CustomProperties.ContainsKey(TurnPropKey)) { return 0; } string propKey = FinishedTurnPropKey + player.ID; return ( int )room.CustomProperties[propKey]; } ///
/// 设置玩家完成的回合 (在房间属性中) /// /// 玩家引用 /// 回合索引 public static void SetFinishedTurn( this PhotonPlayer player, int turn) { Room room = PhotonNetwork.room; if (room == null || room.CustomProperties == null ) { return ; } string propKey = FinishedTurnPropKey + player.ID; Hashtable finishedTurnProp = new Hashtable(); finishedTurnProp[propKey] = turn; room.SetCustomProperties(finishedTurnProp); } } |
0x02 RpsCore | 剪刀石头布核心
RpsCore
组件脚本是这个案例专用的,并实现了一个典型的石头剪刀布游戏的具体规则和数据。它作为视觉界面、游戏逻辑和PunTurnManager
之间的中间人。
它处理PunTurnManager
,实现所有的回调并控制各种UI元素来反映游戏的现状。它还处理用户输入。
在一个特定的游戏中尝试和分离什么是通用的、什么是必要的或不同的,这样的做法总是好的。在开发新功能时要记住这一点。下面是游戏的核心代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 | using System; using System.Collections; using Photon; using UnityEngine; using UnityEngine.UI; using Random = UnityEngine.Random; // Photon服务器为每个玩家指派一个ActorNumber (player.ID),从1开始 // 至于这个游戏,我们不需要实际的数字 // 这个游戏使用0和1,这样客户端需要自己计算出自己的号码 public class RpsCore : PunBehaviour, IPunTurnManagerCallbacks { [Tooltip( "连接UI视图" )] [SerializeField] private RectTransform ConnectUiView; [Tooltip( "游戏UI视图" )] [SerializeField] private RectTransform GameUiView; [Tooltip( "按钮幕布组" )] [SerializeField] private CanvasGroup ButtonCanvasGroup; [Tooltip( "计时器填充图" )] [SerializeField] private RectTransform TimerFillImage; [Tooltip( "回合文本" )] [SerializeField] private Text TurnText; [Tooltip( "时间文本" )] [SerializeField] private Text TimeText; [Tooltip( "远程玩家文本" )] [SerializeField] private Text RemotePlayerText; [Tooltip( "本地玩家文本" )] [SerializeField] private Text LocalPlayerText; [Tooltip( "输赢图片" )] [SerializeField] private Image WinOrLossImage; [Tooltip( "本地选择图片" )] [SerializeField] private Image localSelectionImage; [Tooltip( "本地选择" )] public Hand localSelection; [Tooltip( "远程选择图片" )] [SerializeField] private Image remoteSelectionImage; [Tooltip( "远程选择" )] public Hand remoteSelection; [Tooltip( "已选石头" )] [SerializeField] private Sprite SelectedRock; [Tooltip( "已选纸" )] [SerializeField] private Sprite SelectedPaper; [Tooltip( "已选剪刀" )] [SerializeField] private Sprite SelectedScissors; [Tooltip( "胜利精灵" )] [SerializeField] private Sprite SpriteWin; [Tooltip( "失败精灵" )] [SerializeField] private Sprite SpriteLose; [Tooltip( "平局精灵" )] [SerializeField] private Sprite SpriteDraw; [Tooltip( "断连面板" )] [SerializeField] private RectTransform DisconnectedPanel; private ResultType result; //结果 private PunTurnManager turnManager; //回合管家 [Tooltip( "随机手势" )] public Hand randomHand; // 用于当本地玩家没有选择任何手势时显示远程玩家的手势 // 追踪显示结果的时机来处理游戏逻辑. private bool IsShowingResults; public enum Hand //手势枚举 { None = 0, Rock, //石头 Paper, //纸|布 Scissors //剪刀 } public enum ResultType //结果类型枚举 { None = 0, Draw, //和 LocalWin, //赢 LocalLoss //输 } public void Start() { this .turnManager = this .gameObject.AddComponent(); //添加组件并赋值 this .turnManager.TurnManagerListener = this ; //为监听器赋值,从而触发下面的回调函数来完成游戏逻辑 this .turnManager.TurnDuration = 5f; //初始化回合持续时间 this .localSelectionImage.gameObject.SetActive( false ); //激活本地选择图片 this .remoteSelectionImage.gameObject.SetActive( false ); //激活远程选择图片 this .StartCoroutine( "CycleRemoteHandCoroutine" ); //启动协程,间隔0.5秒随机一个手势 RefreshUIViews(); //刷新UI视图 } public void Update() { // 检查我们是否脱离了环境, 这意味着我们有可能回到演示中枢(演示中枢是用来总控所有案例的). if ( this .DisconnectedPanel == null ) { Destroy( this .gameObject); } // 为了方便调试, 弄一些快捷键是很有用的: if (Input.GetKeyUp(KeyCode.L)) //L键离开房间 { PhotonNetwork.LeaveRoom(); } if (Input.GetKeyUp(KeyCode.C)) //C键连接 { PhotonNetwork.ConnectUsingSettings( null ); PhotonHandler.StopFallbackSendAckThread(); } if ( ! PhotonNetwork.inRoom) //不在房间则退出 { return ; } // 如果PUN已连接或正在连接则禁用"reconnect panel"(重连面板) if (PhotonNetwork.connected && this .DisconnectedPanel.gameObject.GetActive()) { this .DisconnectedPanel.gameObject.SetActive( false ); } if (!PhotonNetwork.connected && !PhotonNetwork.connecting && ! this .DisconnectedPanel.gameObject.GetActive()) { this .DisconnectedPanel.gameObject.SetActive( true ); } if (PhotonNetwork.room.PlayerCount>1) { if ( this .turnManager.IsOver) { return ; //回合结束 } /* // check if we ran out of time, in which case we loose if (turnEnd<0f && !IsShowingResults) { Debug.Log("Calling OnTurnCompleted with turnEnd ="+turnEnd); OnTurnCompleted(-1); return; } */ if ( this .TurnText != null ) { this .TurnText.text = this .turnManager.Turn.ToString(); //更新回合数 } if ( this .turnManager.Turn > 0 && this .TimeText != null && ! IsShowingResults) { this .TimeText.text = this .turnManager.RemainingSecondsInTurn.ToString( "F1" ) + " 秒" ; //更新回合剩余时间 TimerFillImage.anchorMax = new Vector2(1f- this .turnManager.RemainingSecondsInTurn/ this .turnManager.TurnDuration,1f); } } this .UpdatePlayerTexts(); //更新玩家文本信息 // 展示本地玩家的选择手势 Sprite selected = SelectionToSprite( this .localSelection); if (selected != null ) { this .localSelectionImage.gameObject.SetActive( true ); this .localSelectionImage.sprite = selected; } // 远程玩家的选择只在回合结束时(双方都完成回合)展示 if ( this .turnManager.IsCompletedByAll) { selected = SelectionToSprite( this .remoteSelection); if (selected != null ) { this .remoteSelectionImage.color = new Color(1,1,1,1); this .remoteSelectionImage.sprite = selected; } } else { ButtonCanvasGroup.interactable = PhotonNetwork.room.PlayerCount > 1; //玩家数量大于1才可以触发按钮 if (PhotonNetwork.room.PlayerCount < 2) { this .remoteSelectionImage.color = new Color(1, 1, 1, 0); } // 如果所有玩家都没有完成该回合,我们为远程玩家的手势使用一个随机图片 else if ( this .turnManager.Turn > 0 && ! this .turnManager.IsCompletedByAll) { // 远程玩家手势图片的阿尔法值(透明度)被用于表明远程玩家是否“活跃”以及“完成回合” PhotonPlayer remote = PhotonNetwork.player.GetNext(); float alpha = 0.5f; if ( this .turnManager.GetPlayerFinishedTurn(remote)) { alpha = 1; //完成回合为1 } if (remote != null && remote.IsInactive) { alpha = 0.1f; } this .remoteSelectionImage.color = new Color(1, 1, 1, alpha); this .remoteSelectionImage.sprite = SelectionToSprite(randomHand); } } } #region TurnManager Callbacks //回调区域 ///
/// 发起回合开始事件. /// /// 回合. public void OnTurnBegins( int turn) { Debug.Log( "OnTurnBegins() turn: " + turn); this .localSelection = Hand.None; this .remoteSelection = Hand.None; this .WinOrLossImage.gameObject.SetActive( false ); //关闭输赢的图片 this .localSelectionImage.gameObject.SetActive( false ); //关闭本地选择图片 this .remoteSelectionImage.gameObject.SetActive( true ); //关闭远程选择图片 IsShowingResults = false ; //不展示结果 ButtonCanvasGroup.interactable = true ; //可以与按钮交互 } ///
/// 当回合完成时调用(被所有玩家完成) /// /// 回合索引 /// Object. public void OnTurnCompleted( int obj) { Debug.Log( "OnTurnCompleted: " + obj); this .CalculateWinAndLoss(); //计算输赢 this .UpdateScores(); //更新得分 this .OnEndTurn(); //结束回合 } ///
/// 当玩家移动时调用(但是没有完成该回合) /// /// 玩家引用 /// 回合索引 /// 移动对象数据 /// Photon player. public void OnPlayerMove(PhotonPlayer photonPlayer, int turn, object move) { Debug.Log( "OnPlayerMove: " + photonPlayer + " turn: " + turn + " action: " + move); throw new NotImplementedException(); } ///
/// 当玩家完成回合时调用(包括该玩家的动作/移动) /// /// 玩家引用 /// 回合索引 /// 移动对象数据 /// Photon player. public void OnPlayerFinished(PhotonPlayer photonPlayer, int turn, object move) { Debug.Log( "OnTurnFinished: " + photonPlayer + " turn: " + turn + " action: " + move); if (photonPlayer.IsLocal) { this .localSelection = (Hand)( byte )move; } else { this .remoteSelection = (Hand)( byte )move; } } ///
/// 当回合由于时间限制完成时调用(回合超时) /// /// 回合索引 /// Object. public void OnTurnTimeEnds( int obj) { if (!IsShowingResults) { Debug.Log( "OnTurnTimeEnds: Calling OnTurnCompleted" ); OnTurnCompleted(-1); } } ///
/// 更新得分 /// private void UpdateScores() { if ( this .result == ResultType.LocalWin) { PhotonNetwork.player.AddScore(1); //这是PhotonPlayer的扩展方法.就是给玩家加分 } } #endregion #region Core Gameplay Methods //核心玩法 /// 调用来开始回合 (只有主客户端会发送). public void StartTurn() { if (PhotonNetwork.isMasterClient) { this .turnManager.BeginTurn(); } } ///
/// 回合中的选择 /// /// 选择. public void MakeTurn(Hand selection) { this .turnManager.SendMove(( byte )selection, true ); } ///
/// 回合结束 /// public void OnEndTurn() { this .StartCoroutine( "ShowResultsBeginNextTurnCoroutine" ); } ///
/// 显示结果并开始下一回合的协程 /// /// . public IEnumerator ShowResultsBeginNextTurnCoroutine() { ButtonCanvasGroup.interactable = false ; //禁用按钮交互 IsShowingResults = true ; // yield return new WaitForSeconds(1.5f); if ( this .result == ResultType.Draw) //根据结果展示不同的图片 { this .WinOrLossImage.sprite = this .SpriteDraw; } else { this .WinOrLossImage.sprite = this .result == ResultType.LocalWin ? this .SpriteWin : SpriteLose; } this .WinOrLossImage.gameObject.SetActive( true ); yield return new WaitForSeconds(2.0f); this .StartTurn(); } ///
/// 结束游戏 /// public void EndGame() { Debug.Log( "EndGame" ); } ///
/// 计算输赢 /// private void CalculateWinAndLoss() { this .result = ResultType.Draw; if ( this .localSelection == this .remoteSelection) //如果双方的手势一样,则为和局 { return ; } if ( this .localSelection == Hand.None) //如果本地玩家没有选择,弃权为输 { this .result = ResultType.LocalLoss; return ; } if ( this .remoteSelection == Hand.None) //远程玩家没有选择也为输 { this .result = ResultType.LocalWin; } if ( this .localSelection == Hand.Rock) //根据石头剪刀布的游戏规则判断 { this .result = ( this .remoteSelection == Hand.Scissors) ? ResultType.LocalWin : ResultType.LocalLoss; } if ( this .localSelection == Hand.Paper) { this .result = ( this .remoteSelection == Hand.Rock) ? ResultType.LocalWin : ResultType.LocalLoss; } if ( this .localSelection == Hand.Scissors) { this .result = ( this .remoteSelection == Hand.Paper) ? ResultType.LocalWin : ResultType.LocalLoss; } } ///
/// 选择精灵 /// /// 返回对应手势的精灵. /// 手势. private Sprite SelectionToSprite(Hand hand) { switch (hand) { case Hand.None: break ; case Hand.Rock: return this .SelectedRock; case Hand.Paper: return this .SelectedPaper; case Hand.Scissors: return this .SelectedScissors; } return null ; } ///
/// 更新玩家文本信息 /// private void UpdatePlayerTexts() { PhotonPlayer remote = PhotonNetwork.player.GetNext(); PhotonPlayer local = PhotonNetwork.player; if (remote != null ) { // 应该是这种格式: "name 00" this .RemotePlayerText.text = remote.NickName + " " + remote.GetScore().ToString( "D2" ); } else { TimerFillImage.anchorMax = new Vector2(0f,1f); this .TimeText.text = "" ; this .RemotePlayerText.text = "等待其他玩家 00" ; } if (local != null ) { // 应该是这种样式: "YOU 00" this .LocalPlayerText.text = "YOU " + local.GetScore().ToString( "D2" ); } } public IEnumerator CycleRemoteHandCoroutine() { while ( true ) { // 循环可用的图像 this .randomHand = (Hand)Random.Range(1, 4); yield return new WaitForSeconds(0.5f); } } #endregion #region Handling Of Buttons //处理按钮 ///
/// 点击石头按钮就是选择石头,下同 /// public void OnClickRock() { this .MakeTurn(Hand.Rock); } public void OnClickPaper() { this .MakeTurn(Hand.Paper); } public void OnClickScissors() { this .MakeTurn(Hand.Scissors); } ///
/// 连接 /// public void OnClickConnect() { PhotonNetwork.ConnectUsingSettings( null ); PhotonHandler.StopFallbackSendAckThread(); // 这在案例中被用于后台超时! } ///
/// 重新连接并重新加入 /// public void OnClickReConnectAndRejoin() { PhotonNetwork.ReconnectAndRejoin(); PhotonHandler.StopFallbackSendAckThread(); // this is used in the demo to timeout in background! } #endregion ///
/// 刷新UI视图 /// void RefreshUIViews() { TimerFillImage.anchorMax = new Vector2(0f,1f); ConnectUiView.gameObject.SetActive(!PhotonNetwork.inRoom); GameUiView.gameObject.SetActive(PhotonNetwork.inRoom); ButtonCanvasGroup.interactable = PhotonNetwork.room!= null ?PhotonNetwork.room.PlayerCount > 1: false ; } ///
/// 当本地用户/客户离开房间时调用。 /// /// 当离开一个房间时,PUN将你带回主服务器。 /// 在您可以使用游戏大厅和创建/加入房间之前,OnJoinedLobby()或OnConnectedToMaster()会再次被调用。 public override void OnLeftRoom() { Debug.Log( "OnLeftRoom()" ); RefreshUIViews(); } ///
/// 当进入一个房间(通过创建或加入)时被调用。在所有客户端(包括主客户端)上被调用. /// /// 这种方法通常用于实例化玩家角色。 /// 如果一场比赛必须“积极地”被开始,你也可以调用一个由用户的按键或定时器触发的PunRPC 。 /// /// 当这个被调用时,你通常可以通过PhotonNetwork.playerList访问在房间里现有的玩家。 /// 同时,所有自定义属性Room.customProperties应该已经可用。检查Room.playerCount就知道房间里是否有足够的玩家来开始游戏. public override void OnJoinedRoom() { RefreshUIViews(); if (PhotonNetwork.room.PlayerCount == 2) { if ( this .turnManager.Turn == 0) { // 当房间内有两个玩家,则开始首回合 this .StartTurn(); } } else { Debug.Log( "Waiting for another player" ); } } ///
/// 当一个远程玩家进入房间时调用。这个PhotonPlayer在这个时候已经被添加playerlist玩家列表. /// /// 如果你的游戏开始时就有一定数量的玩家,这个回调在检查Room.playerCount并发现你是否可以开始游戏时会很有用. /// New player. public override void OnPhotonPlayerConnected(PhotonPlayer newPlayer) { Debug.Log( "Other player arrived" ); if (PhotonNetwork.room.PlayerCount == 2) { if ( this .turnManager.Turn == 0) { this .StartTurn(); } } } ///
/// 当一个远程玩家离开房间时调用。这个PhotonPlayer 此时已经从playerlist玩家列表删除. /// /// 当你的客户端调用PhotonNetwork.leaveRoom时,PUN将在现有的客户端上调用此方法。当远程客户端关闭连接或被关闭时,这个回调函数会在经过几秒钟的暂停后被执行. /// Other player. public override void OnPhotonPlayerDisconnected(PhotonPlayer otherPlayer) { Debug.Log( "Other player disconnected! isInactive: " + otherPlayer.IsInactive); } ///
/// 当未知因素导致连接失败(在建立连接之后)时调用,接着调用OnDisconnectedFromPhoton()。 /// /// 如果服务器不能一开始就被连接,就会调用OnFailedToConnectToPhoton。错误的原因会以DisconnectCause的形式提供。 /// Cause. public override void OnConnectionFail(DisconnectCause cause) { this .DisconnectedPanel.gameObject.SetActive( true ); } }
|
RpsDemoConnect
组件和常规的PUN项目一样,都是做连接、加入房间和游戏大厅。
然而,它包含了一个非常重要的功能:重新加入一个房间,这是回合制游戏必备的。这个想法是跟踪用户正在游戏的房间,以便重新连接以及我们发现我们确实之前连接到的一个房间时,我们回到那个房间。
这是通过使用PhotonNetwork.ReJoinRoom(string roomName)来完成的,且它依赖于RoomOptions.PlayerTtl (玩家生存时间)来成功调用重新加入。举个栗子,如果你的游戏允许用户断连几天,你将需要设置RoomOptions.PlayerTtl为"(天数) * 24 *60 * 60 * 1000"。
RpsDemoConnect
组件满足了连接和游戏的基本要求。在一个实际的项目中,您可能会希望在这方面进行扩展,以提供对各种连接状态、游戏大厅、所有社交方面(如好友列表)等的更多反馈。这方面的游戏开发对于绝大多数网络游戏、回合制游戏或非回合制游戏来说都是一样的。
连接部分的代码属于是PUN的基础部分,这里的代码比较常规:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | using Photon; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; public class RpsDemoConnect : PunBehaviour { public InputField InputField; public string UserId; public string previousRoom; private const string MainSceneName = "DemoRPS-Scene" ; const string NickNamePlayerPrefsKey = "NickName" ; void Start() { InputField.text = PlayerPrefs.HasKey(NickNamePlayerPrefsKey)?PlayerPrefs.GetString(NickNamePlayerPrefsKey): "" ; //三元运算,如果PlayerPrefs中有玩家的昵称则获取之,否则为空字符串 } ///
/// 应用用户Id并连接 /// public void ApplyUserIdAndConnect() { string nickName = "DemoNick" ; //设置玩家的昵称 if ( this .InputField != null && ! string .IsNullOrEmpty( this .InputField.text)) { nickName = this .InputField.text; PlayerPrefs.SetString(NickNamePlayerPrefsKey,nickName); } //if (string.IsNullOrEmpty(UserId)) //{ // this.UserId = nickName + "ID"; //} Debug.Log( "Nickname: " + nickName + " userID: " + this .UserId, this ); if (PhotonNetwork.AuthValues == null ) { PhotonNetwork.AuthValues = new AuthenticationValues(); } //else //{ // Debug.Log("Re-using AuthValues. UserId: " + PhotonNetwork.AuthValues.UserId); //} PhotonNetwork.playerName = nickName; PhotonNetwork.ConnectUsingSettings( "0.5" ); // this way we can force timeouts by pausing the client (in editor) PhotonHandler.StopFallbackSendAckThread(); } ///
/// 在到主服务器连接被建立和认证后调用,但是只有当PhotonNetwork.autoJoinLobby是false时才调用. /// /// 如果你设置PhotonNetwork.autoJoinLobby为true,取而代之调用的是OnJoinedLobby(). /// /// 即使没有在游戏大厅内,你也可以加入房间和创建房间。在这种情况下使用了默认的大厅。 /// 可用房间列表将不可用,除非你通过PhotonNetwork.joinLobby加入一个游戏大厅. public override void OnConnectedToMaster() { // 连接之后 this .UserId = PhotonNetwork.player.UserId; ////Debug.Log("UserID " + this.UserId); // 超时之后: 重新加入之前的房间(如果有的话) if (! string .IsNullOrEmpty( this .previousRoom)) { Debug.Log( "ReJoining previous room: " + this .previousRoom); PhotonNetwork.ReJoinRoom( this .previousRoom); this .previousRoom = null ; // we only will try to re-join once. if this fails, we will get into a random/new room } else { // 否则加入随机房间 PhotonNetwork.JoinRandomRoom(); } } ///
/// 在主服务器上进入一个大厅时调用。实际的房间列表的更新会调用OnReceivedRoomListUpdate()。 /// /// 注意:当PhotonNetwork.autoJoinLobby是false时,OnConnectedToMaster()将会被调用并且房间列表将不可用。 /// /// 而在大堂的房间列表是在固定的时间间隔内自动更新(这是你不能修改的)。 /// 当OnReceivedRoomListUpdate()在OnJoinedLobby()之后被调用后,房间列表变得可用. public override void OnJoinedLobby() { OnConnectedToMaster(); // 这样我们是否加入游戏大厅都无所谓了 } ///
/// 在一个JoinRandom()请求失败后调用。参数提供ErrorCode错误代码和消息。 /// /// Code and message. public override void OnPhotonRandomJoinFailed( object [] codeAndMsg) { PhotonNetwork.CreateRoom( null , new RoomOptions() { MaxPlayers = 2, PlayerTtl = 5000 }, null ); } ///
/// 当进入一个房间(通过创建或加入)时被调用。在所有客户端(包括主客户端)上被调用. /// /// 这种方法通常用于实例化玩家角色。 /// 如果一场比赛必须“积极地”被开始,你也可以调用一个由用户的按键或定时器触发的PunRPC 。 /// /// 当这个被调用时,你通常可以通过PhotonNetwork.playerList访问在房间里现有的玩家。 /// 同时,所有自定义属性Room.customProperties应该已经可用。检查Room.playerCount就知道房间里是否有足够的玩家来开始游戏. public override void OnJoinedRoom() { Debug.Log( "Joined room: " + PhotonNetwork.room.Name); this .previousRoom = PhotonNetwork.room.Name; } ///
/// 当一个JoinRoom()调用失败时被调用。参数以数组的方式提供ErrorCode和消息。 /// /// 最有可能是因为房间的名称已经在使用(其他客户端比你更快)。 /// 如果PhotonNetwork.logLevel >= PhotonLogLevel.Informational为真,PUN会记录一些信息。 /// codeAndMsg[0]是short ErrorCode,codeAndMsg[1]是调试消息字符串. public override void OnPhotonJoinRoomFailed( object [] codeAndMsg) { this .previousRoom = null ; } ///
/// 当未知因素导致连接失败(在建立连接之后)时调用,接着调用OnDisconnectedFromPhoton()。 /// /// 如果服务器不能一开始就被连接,就会调用OnFailedToConnectToPhoton。错误的原因会以DisconnectCause的形式提供。 /// Cause. public override void OnConnectionFail(DisconnectCause cause) { Debug.Log( "Disconnected due to: " + cause + ". this.previousRoom: " + this .previousRoom); } } |
最后附加PunPlayerScores玩家分数和扩展 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | using System.Collections.Generic; using UnityEngine; using System.Collections; using Hashtable = ExitGames.Client.Photon.Hashtable; public class PunPlayerScores : MonoBehaviour { public const string PlayerScoreProp = "score" ; } public static class ScoreExtensions { ///
/// 设置分数 /// /// 玩家. /// 分数. public static void SetScore( this PhotonPlayer player, int newScore) { Hashtable score = new Hashtable(); // using PUN's implementation of Hashtable score[PunPlayerScores.PlayerScoreProp] = newScore; player.SetCustomProperties(score); // this locally sets the score and will sync it in-game asap. } ///
/// 加分 /// /// 玩家. /// 加的分数. public static void AddScore( this PhotonPlayer player, int scoreToAddToCurrent) { int current = player.GetScore(); current = current + scoreToAddToCurrent; Hashtable score = new Hashtable(); // 使用PUN的Hashtable实现 score[PunPlayerScores.PlayerScoreProp] = current; player.SetCustomProperties(score); // 这会在本地设置分数并尽快地在游戏内同步. } ///
/// 获取得分. /// /// 返回分数. /// 玩家. public static int GetScore( this PhotonPlayer player) { object score; if (player.CustomProperties.TryGetValue(PunPlayerScores.PlayerScoreProp, out score)) { return ( int ) score; } return 0; } } |
至于游戏的运行测试,大家可以连接试玩一下,因为这个游戏我们从小就会玩,这里就不赘述了!