Unity3d:多人在线VR游戏实战序章-PUN回合制游戏案例详解

发表于2017-03-20
评论2 4.8k浏览

如何使用unity3D开发一款多人在线的VR游戏,本文档给出了一个如何实现这样的游戏的概述,以PUN回合制游戏为案例,帮助那些项目开发人员。

PUN回合制游戏案例

提纲

0x00 前言................................................................................................................ 3

0x01 PunTurnManager脚本................................................................................... 3

Ø IPunTurnManagerCallbacks回调函数...................................................... 4

Ø TurnManager Extensions |TurnManager扩展........................................ 4

0x02 RpsCore | 剪刀石头布核心.............................................................................. 5

0x03 RpsDemoConnect.......................................................................................... 6

 

PhotonUnity Networking (PUN) 可用于回合制游戏,如策略游戏或棋类游戏。本文档给出了一个如何实现这样的游戏的概述,基于我们的Rock Paper Scissors Demo [1],该案例在PUN包中。

1 石头剪刀布游戏预览

演示围绕几个组件脚本:PunTurnManagerRpsCoreRpsDemoConnect

[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;
    }
}

至于游戏的运行测试,大家可以连接试玩一下,因为这个游戏我们从小就会玩,这里就不赘述了!

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引