在线捉鬼游戏开发之三 - 代码与测试(玩家发言)

-----------回顾分割线-----------

此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式、重构的理解。

索引目录

0. 索引(持续更新中)

1. 游戏流程介绍与技术选用

2. 设计业务对象与对象职责划分(1)(图解旧版本)

3. 设计业务对象与对象职责划分(2)(旧版本代码剖析)

4. 设计业务对象与对象职责划分(3)(新版本业务对象设计)

5. 业务对象核心代码编写与单元测试(游戏开始前:玩家入座与退出)

6. 业务对象核心代码编写与单元测试(游戏开始:抽题、分角色、开启鬼讨论模式)

7. 代码与测试(鬼讨论、鬼投票)

8. 代码与测试(玩家发言)

-----------回顾结束分割线-----------

先放上源代码,svn地址:https://115.29.246.25/svn/Catghost/

账号:guest 密码:guest(支持源代码下载,已设只读权限,待我基本做出初始版本后再放到git)

-----------本篇开始分割线----------

依旧是按照顺序图来完成(越发感觉到类图、顺序图给代码带来的指导性意义)

1. 玩家发言

顺序图中的1-3步:CurrentSpeaker发言后,SpeakManager记录并显示出来,同时设置下一个允许发言的玩家。

在SpeakManager中其实已经写好了大部分PlayerSpeak()的内容,只需加一行SetNextSpeaker()即可。

public void PlayerSpeak(Player player, string str)
{
    if (IsGhostDiscussing())
    {
        CheckGhostSpeaker(player);
    }
    else
    {
        CheckCurrentSpeaker(player);
    }
    AddToRecord(FormatSpeak(player.NickName, str));
    SetNextSpeaker(player);
}

/// <summary>
/// 设置下一位发言人
/// </summary>
/// <param name="currentPlayer">当前发言人</param>
private void SetNextSpeaker(Player currentPlayer)
{
    Player[] players = GetPlayerManager().GetAllPlayerArray();
    Player nextPlayer = null;
    for (int i = 0; i < players.Length; i++)
    {
        if (players[i].Equals(currentPlayer))
        {
            if (i == players.Count() - 1)
            {
                nextPlayer = players[0];
                break;
            }
            nextPlayer = players[i + 1];
            break;
        }
    }
    SetSpeaker(nextPlayer);
}

测试代码如下:用for来测试是否满足循环发言时的SetNextSpeaker

[TestMethod]
public void PlayerSpeakUnitTest()
{
    JoinGame();

    // ghost discussing...

    Player electPlayer = GetPlayerManager().GetAllPlayerArray()[5]; // elect player
    GhostVoting(electPlayer);

    for (int i = 0; i < 2; i++)
    {
        PlayerSpeaking(electPlayer, i);
    }

    ShowPlayerListen();
}

// private method

private void PlayerSpeaking(Player starter, int times)
{
    bool canSpeak = false;
    if (times > 0) canSpeak = true;
    foreach (Player p in GetPlayerManager().GetAllPlayerArray())
    {
        if (p.Equals(starter))
        {
            canSpeak = true;
        }
        if (canSpeak)
        {
            p.Speak("i‘m " + p.NickName);
        }
    }
}

测试结果如期所至:所有玩家都能看到,且从第6个玩家(kimi)开始发言,两轮后结束。因为没加入LoopManager进行监控,所以要到vivian才结束。加入LoopManager后应该在coco发言完就结束了。

再看代码度量值,貌似还有进步的空间:判断太多导致圈复杂度上升,类耦合可适当减少。

先看当前的SetNextSpeaker()代码

/// <summary>
/// 设置下一位发言人
/// </summary>
/// <param name="currentPlayer">当前发言人</param>
private void SetNextSpeaker(Player currentPlayer)
{
    Player[] players = GetPlayerManager().GetAllPlayerArray();
    Player nextPlayer = null;
    for (int i = 0; i < players.Length; i++)
    {
        if (players[i].Equals(currentPlayer))
        {
            if (i == players.Count() - 1)
            {
                nextPlayer = players[0];
                break;
            }
            nextPlayer = players[i + 1];
            break;
        }
    }
    SetSpeaker(nextPlayer);
}

感觉最内层的if(已表粗体)只是为了简单判断如果循环到数组末尾,则从第一个开始。坏味道出现了——这是SpeakManager该做的事情吗?整个SetNextSpeaker()的主要任务是设置下一个玩家允许发言,你就别给我整当前玩家是谁,你直接给我来下一个玩家是谁不就得了?所以,整个SetNextSpeaker()都跨越了自己的职责,占用了谁的职责呢?谁最清楚下一个玩家是谁呢?Player自己知道吗——不知道,只有玩家管理者PlayerManager知道,因为他就是干这个的——维护玩家列表!

题外提一句,这里很容易想到状态模式——Player说完自动换下一位Player,即CurrentSpeaker标记的状态在改变——但很遗憾,这里并不合适:首先,Player自己不应该知道自己下一位是谁,而是PlayerManager才知道,且如果Player知道自己的下一位,那么他就有权决定下一位是谁(状态模式是为了易于轻松增/改传递的下一个状态),这就与游戏规则不符了。故,还是需要一个统领全局的局外人PlayerManager来操作(有点儿建造者模式中Builder的味道)。

首先在PlayerManager中增加GetNextPlayer()方法:

/// <summary>
/// 返回下一位玩家
/// </summary>
/// <param name="currentPlayer">当前玩家</param>
/// <returns>下一位玩家</returns>
public Player GetNextPlayer(Player currentPlayer)
{
    CheckPlayer(currentPlayer);
    Player result = null;
    for (int i = 0; i < GetAllPlayerArray().Length; i++)
    {
        if (GetAllPlayerArray()[i].Equals(currentPlayer))
        {
            if (i == GetAllPlayerArray().Length - 1)
            {
                result = GetAllPlayerArray()[0];
            }            else            {
                result = GetAllPlayerArray()[i + 1];            }
        }
    }
    return result;
}

对应SpeakManager中的SetNextSpeaker()将非常简单:

/// <summary>
/// 设置下一位发言人
/// </summary>
/// <param name="currentPlayer">当前发言人</param>
private void SetNextSpeaker(Player currentPlayer)
{
    Player nextPlayer = GetPlayerManager().GetNextPlayer(currentPlayer);
    SetSpeaker(nextPlayer);
}

代码度量值方面:减少了类耦合(将不属于的职责分离出去了),但圈复杂度传给了PlayerManager。需要继续优化:

我们可以观察到寻找下一位玩家的关键就在于座位号,无论是对当前玩家的判断,还是下一位玩家的筛选,都要通过座位号,所以考虑提取出GetSeatOrder()方法:

public Player GetNextPlayer(Player currentPlayer)
{
    Player[] players = GetAllPlayerArray();
    int currentSeatOrder = GetSeatOrder(currentPlayer);
    return currentSeatOrder == players.Length - 1 ? players[0] : players[currentSeatOrder + 1];
}

/// <summary>
/// 返回玩家座位号
/// </summary>
/// <param name="player">玩家</param>
/// <returns>座位号</returns>
private int GetSeatOrder(Player player)
{
    CheckPlayer(player);
    for (int i = 0; i < GetAllPlayerArray().Length; i++)
    {
        if (GetAllPlayerArray()[i].Equals(player))
        {
            return i;
        }
    }
    return -1;
}

测试之,没问题。再看代码度量值:很好,算是完成了玩家发言的部分。

2. 循环管理

顺序图中的4-6步:设置下一允许发言的玩家后,LoopManager负责检查是否此循环结束(首轮发言有两个循环),若没结束,则不做操作;若已结束,则SpeakManager发出系统指令告诉大家开始投票,投票环节不允许发言,故要对CurrentSpeaker做一些处理。

首先在SpeakManager.SetNextSpeaker()的时候增加CheckIsEnd()检查:如果发言完了,则设置当前允许发言的人为空,且系统发出提示。

/// <summary>
/// 设置下一位发言人
/// </summary>
/// <param name="currentPlayer">当前发言人</param>
private void SetNextSpeaker(Player currentPlayer)
{
    Player nextPlayer = GetPlayerManager().GetNextPlayer(currentPlayer);
    SetSpeaker(nextPlayer);
    ChechIsLoopEnd(nextPlayer);
}

/// <summary>
/// 检查是否循环结束
/// </summary>
/// <param name="currentPlayer">当前玩家</param>
private void ChechIsLoopEnd(Player currentPlayer)
{
    if (GetLoopManager().IsLoopEnd(currentPlayer))
    {
        SetSpeaker(null);
        SystemSpeak(GetSetting().GetAppSettingValue("VoteTip"));
    }
}

接着在LoopManager中填充IsLoopEnd()方法:其注意第一轮是发言两圈。

public bool IsLoopEnd(Player currentPlayer)
{
    if (currentPlayer.Equals(this._loopStarter))
    {
        if (_isFirstLoop)
        {
            _isFirstLoop = false;
            return false;
        }
        return true;
    }
    return false;
}

测试结果需要做一些小调整:把SpeakManager.CheckCurrentSpeaker()的“不许场外”的异常先禁用,否则会报错看不到输出。

可以看到,首轮每人发言了两次,且第二论(及以后)每人只能发言一次,最后框外的kimi-vivian的发言是因为异常未阻止导致的,符合预期。测试通过。代码度量值也是ok,就不贴图了。

到此,循环管理也算完成。

也许朋友们会问:那异常的处理在最后要怎么办?是返回string,还是终止程序?还是不予处理?——这些都在考虑ui的时候在考虑,此环节仅作核心Models代码编写。千万不能一时混淆太多考虑——饭要一口一口吃,代码要一处一处写

时间: 2024-07-31 06:06:49

在线捉鬼游戏开发之三 - 代码与测试(玩家发言)的相关文章

在线捉鬼游戏开发之三 - 代码与测试(鬼讨论、鬼投票)

-----------回顾分割线----------- 此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式.重构的理解. 索引目录: 0. 索引(持续更新中) 1. 游戏流程介绍与技术选用 2. 设计业务对象与对象职责划分(1)(图解旧版本) 3. 设计业务对象与对象职责划分(2)(旧版本代码剖析) 4. 设计业务对象与对象职责划分(3)(新版本业务对象设计) 5. 业务对象核心代码编写与单元测试

在线捉鬼游戏开发之三 - 代码与测试(玩家投票)

-----------回顾分割线----------- 此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式.重构的理解. 索引目录: 0. 索引(持续更新中) 1. 游戏流程介绍与技术选用 2. 设计业务对象与对象职责划分(1)(图解旧版本) 3. 设计业务对象与对象职责划分(2)(旧版本代码剖析) 4. 设计业务对象与对象职责划分(3)(新版本业务对象设计) 5. 业务对象核心代码编写与单元测试

在线捉鬼游戏开发之三 - 业务对象核心代码编写与单元测试(游戏开始:抽题、分角色、开启鬼讨论模式)

-----------回顾分割线----------- 系列之一讲述了游戏规则,系列之二讲述了旧版的前台效果.代码中不好的地方.以及新版的改进核心,此篇开始就是新版代码编写全过程.此系列旨在开发类似“谁是卧底+杀人游戏”的捉鬼游戏在线版,记录从分析游戏开始的开发全过程,通过此项目让自己熟悉面向对象的SOLID原则,提高对设计模式.重构的理解. 索引目录: 0. 索引(持续更新中) 1. 游戏流程介绍与技术选用 2. 设计业务对象与对象职责划分(1)(图解旧版本) 3. 设计业务对象与对象职责划分

在线捉鬼游戏开发之一 - 游戏流程介绍与技术选用

看过芒果台某知名节目的朋友们应该对“谁是卧底”不会陌生:N人参与,N-1人拿到同一个词(如馒头),剩余一人拿到另一个词(如包子),N人都只能看到自己的词,故没人知道自己是否和别人描述的不一样.游戏采用轮流发言制,想尽办法描述自己手中的词,让自己不被怀疑,且又不能让真正的卧底猜出自己和别人不一样,直到猜出谁是卧底. 此类游戏的有趣之处在于描述的尺度要拿捏在明确且不点破之间,机器没有人的智慧那么发散,人的参与感就更重要了(不然好比我喜欢的飞行棋,你还真不知道网络对面的对手是不是条狗). 捉鬼 这个游

在线捉鬼游戏开发之二 - 设计业务对象与对象职责划分(3)

“回忆总是残酷的”——在“设计业务对象与对象职责划分(2)”中,对旧版本的代码进行了剖析,也发现了不少臭味道,本篇将记录我是如何建设新版的业务对象职责划分. 一.复习设计模式 当初自学设计模式的路径是:从<大话设计模式>开始(做了笔记),到Gof的<设计模式>,再到辛勤网友们的各篇总结日志(只看C#的可能会有些局限~).此后,每当我有需要更新代码的时候,或者觉得不太记得清23种经典设计模式的时候,我就会回翻我的笔记,主要看:模式目的.应用场景,以最快速度在脑子里回放.在复习的同时,

unity游戏开发之自定义事件测试demo

上文中写了unity游戏开发自定义消息事件点击打开链接 下面是测试demo 第一,打卡unity,新建一个场景,然后新建一个空的游戏对象,如图中的EventObj 第二步,测试代码EventTest.as,直接拖拽给上面的空游戏对象EentObj 测试代码如下: using UnityEngine; using System.Collections; public class EventTest : MonoBehaviour { // Use this for initialization v

5、Cocos2dx 3.0小游戏开发的例子寻找测试三个简单的介绍和总结

繁重的劳动开发商,当转载请注明出处:http://blog.csdn.net/haomengzhu/article/details/27186557 測试例子简单介绍 Cocos2d-x 为我们提供了十分丰富的測试例子,这些測试例子是在引擎开发过程中为測试引擎的正确性而编写的代码,同一时候也是演示引擎各个部分怎样使用的良好演示例子. 打开Cocos2d-x根文件夹下build文件夹下的cocos2d-win32.vc2012.sln解决方式,设置当中的 cpp-tests 项目为启动项目.成功执

【Unity NGUI游戏开发之三】TweenPosition位移动画(二):相对于UIAnchor不同分辨率下的完美适配位移动画

Unity中的UI我们采用的是NGUI,NGUI的界面位移动画,我们一般使用的是TweenPosition. 一种是简单的相对位移,不考虑分辨率适配问题,只需要简单的从位置A到位置B,已经在文中介绍了: [Unity NGUI游戏开发之二]TweenPosition位移动画(一):不相对于Anchor的位移动画 另外一种是考虑到屏幕分辨率适配的位移动画,我们游戏中大多遇到的是这种情况. eg.我们想让一个UI从屏幕外沿着屏幕的左边移动到屏幕的中央,TweenPositon播放动画,在960*64

【UNITY3D 游戏开发之三】NGUI &amp;&amp; HUDText 的练习源码及资源

本站文章均为李华明Himi原创,转载务必在明显处注明:(作者新浪微博:@李华明Himi) 转载自[黑米GameDev街区] 原文链接: http://www.himigame.com/unity3d-game/1584.html ? 点击订阅 ? 本博客最新动态!及时将最新博文通知您! 不知道有多久没写博客了,竟然有种怀念的感觉 = =. 从今天开始呢,我会陆续更新一些U3D的小教程,主要以备注交流的心态来写了,原因如下: 1. 我也是u3d新手,只能给童鞋们简单的总结一些东西.或者说是备注给自