/Gwent

Gwent game for 2017 Qt semester.

Primary LanguageC++

Gwent 项目文档

程序模块

程序模块结构

  • Models 为与卡牌、天气、战场、卡组等相关的类;

    • Meta 为卡牌不随着局势改变的静态信息,例如卡片的名称、初始战力、颜色等;

    • Card 为放在游戏中的某张具体的卡牌动态信息,例如它的当前战力、它的 ID 等。

    • Containers 为游戏中各种卡牌的集合的类:

      游戏中,每一张牌会有一个独一无二的 ID ,这个 ID 会跟随卡牌直到整场游戏打完。

      • CardGroup 卡组类,内部存放的是 Meta。
      • CardContainer 存放不在战场上的卡,如手牌/牌组/墓地等,存的是 ID 。
      • BattleLine 存放某个具体的战排中的单位卡,存的是 ID 。
      • BattleSide 存放某一方的三个战排,外加手牌/牌组/墓地/已被移出的牌。
      • BattleField 存放两个 BattleSide 并对其进行管理。
      • CardManager 负责维护 ID 和具体的卡牌之间的映射关系。
    • Assets 中存放着所有的卡牌对应的类。每一张卡牌都有一个独立的类与之对应,这些类一般被用来处理各张牌自己的技能和处理各种场上发生的事件。

  • Controllers 为控制游戏流程相关的类。

    • Network 为和网络相关的类,具体后面会写。
    • RemoteController 为控制服务端的行为的一个类。在我的网络对战的实现中,服务器基本上不需要处理游戏逻辑,主要职责是处理信息的转发和处理。
    • GameController 为控制整个游戏流程的类,其中有几类方法,如负责移动卡牌位置或插入卡牌的方法,负责洗牌、抽牌和重抽的方法,以及负责控制游戏进度的方法。
    • InteractingController 为负责用户交互,输入输出的类。我将游戏中用户可能进行的操作抽象成了几种,例如“获取用户点击了哪个战排”,“获取用户希望将卡牌部署到哪个位置”等等。在文档写到这里之前所有的类都是和具体的 GUI 无关的,它们通过 InteractingController 来获得外界的信息,随着 InteractingController 的实现方式不同可以做出外表不同的昆特牌游戏。
  • Views 为图形用户界面相关的类。

    • States 为图形用户界面的状态类。例如主菜单是一个状态,游戏界面又是一个状态。我将每个状态都写成了一个类。每个类将要自己进行相关的交互的逻辑处理。

模块间的逻辑关系

整个项目大致可以分成客户端和服务端,其中大部分逻辑都运行在客户端。

客户端可以分成三个部分,负责卡牌存储和卡牌技能处理的 Models ,负责控制游戏流程、网络通信、用户交互的 Controllers ,以及负责显示用户界面、处理控件的交互的 Views 。整个客户端是 Controllers 在管理游戏的进度,操纵 Models ,同时从服务端以及通过 Views 来获得外界的各种输入。

服务端的职责很简单,就是不断的接收来自客户端的信息,如果它自己对这个信息不感兴趣的话就把这个信息告诉所有的监听者(不止是发给另一个玩家,是希望留出接口能够给“观战”功能使用),如果感兴趣,则拦截下来处理一下,然后发出对应的处理之后的信息给所有监听者(然后监听者自己决定怎么利用这种广播式的信息)。

程序运行的流程

为了打一把完整的游戏,需要先开启服务端然后开启客户端。

服务端运行流程

开启服务端,输入端口后,服务端将会在这个端口上开始监听,接受传送过来的字符串。同时这个服务端会维护一个存放 IP 地址和端口号的列表,后面称之为听众列表。

  • 如果它解析到来的信息,发现对方希望将自己加入到听众列表中,就继续解析这个信息获取听众的 IP 地址和端口号,然后将它加入到听众列表中。
  • 如果它解析到来的信息,发现对方希望将自己移出听众列表,就解析出它的 IP 地址和端口号然后将其移除。
  • 否则,它会认为这个信息是自己不感兴趣的,会将这个信息发送给听众列表中的所有人让它们自行决定怎么使用这个信息。

以上是 Network 中的 Server 类所具有的功能,但是实际游戏中只有这个功能是不够的,我希望能够弄一个抽象的服务器类,这个类不知道自己会被用作游戏。实际游戏时开启的是 RemoteController ,它还对下列信息感兴趣:

  • 每个玩家在玩的时候,客户端会向它申请一个玩家编号,它会进行回复。同时,当第二个人向它申请编号的时候,它会随机出一个先后手顺序然后发给两个玩家。
  • 客户端在运行过程中有同步的需求,即一个玩家的程序到了某个特定的位置时,如果另一个玩家的程序还没有执行到这里,就会进行等待直到同步。服务端会协助这种同步。

客户端运行流程

简单起见只写进行游戏时的运行流程。

一方客户端开始后,会先加载 MainMenuState ,在玩家点击开始游戏后会询问玩家服务器的 IP 地址和端口号,以及玩家使用的卡组。随后会初始化一个 GameControllerGameController 会初始化一个 InteractingController 负责交互、一个 BattleField 负责存卡牌数据和一个 CardManager 负责管理卡牌。随后它会根据填入的信息连接服务器申请玩家编号,同时初始化自己的卡组。当它申请到了玩家编号后,会判断其它玩家是否就绪以决定是否进行等待。当双方都就绪的时候,双方都会解析服务器传来的先后手顺序信息来决定自己的先后手。随后游戏开始初始化。

初始化过程中,客户端会进行洗牌。然后询问玩家的换牌操作。换牌完成后,两边的每张卡牌所在的位置实际上已经确定,两边都会将自己的信息发送给对方,此时客户端知道了整个场上的信息。

在接下来的过程中总有一个玩家是在进行操作,另一个玩家在等待。

  • 进行操作的玩家通过 InteractingController 进行各种输入,同时这个玩家的每张实例化了的卡牌就在进行各种技能操作和更新。对于每一个原子的操作,都会触发一次对 GamePlayingState 的视图更新、一次让对手进行更新的请求和一个短时间的空事件循环(防止视图更新太快看起来像一次性结算完成)。当所有的操作都结算完成后,发出结算完成的消息至另一方,同时开始等待另一方完成操作。
  • 进行等待的玩家,如果收到了对方的更新请求就会更新自己的数据同时刷新视图。如果收到了对方的操作完成的信息就会让自己进入操作状态。等待时,它还会不断检查这一轮游戏是否已经结束(条件是双方都放弃了跟牌操作)。

当一轮游戏结束后,双方通过同步的机制保证两边可以同步地进行以下的操作:

  • 结算场上的战力
  • 决定下一轮的先后手
  • 移除场上的单位、触发它们的遗愿

随后,开启新的一轮,知道游戏结束,通过 InteractingController 跳出结束界面然后注销在服务端的听众身份。

各功能演示方法(注意事项)

  • 卡组编辑:在编辑卡组界面右方选择好每种卡要多少张,然后点击左边相关的按钮。其中保存功能会将卡组保存成一个文件。导入功能需要先将卡组字符串粘贴到下方的文本框中,导出功能会在文本框中显示导出的字符串。
  • 卡牌重抽:实现逻辑为,在有非同名牌的情况下,会从卡组顶端抽一张,将换下的牌放到卡组最下方。换下的牌的同名牌在整个游戏中都不会被再次重抽到。(可以改成在这一局中不会被重抽到。)
  • 双人对战:通过网络进行对战。实现方法为用 TCP 不断同步两边的状态。
  • 轮流出牌与卡片放置位置:注意,在放置卡片的位置的时候需要将鼠标点到两张卡中间,不能点到卡上,否则不能触发相关的判断。
  • 放弃跟牌:放弃跟牌操作是点一下就会触发的,不是长按。
  • 服务端程序:就是上面提到的那个功能较少的程序(如果能算是服务端程序的话)。

操作注意事项

因为开始写界面的时候已经临近 deadline ,所以用户交互上面有些需要注意的地方,列举如下:

  • 选择要打出哪张卡牌的时候,一旦作出选择,无法撤销,必须将这张牌打出去。
  • 要打出的牌如果是单位牌,就选择打入的地方。这个过程中不能点到其它卡上,否则无法触发判定。
  • 对于能够移动其它牌的牌,需要先点一下要移动的牌,再点击一下要移动到的位置。
  • 类似贝克尔的扭曲之境这种打出立即触发的牌,在点选该牌后还是要在场上随便选择一个位置。
  • 有召唤、抽卡、生成之类的技能触发时,会进入一个新的选择卡牌的 State ,在希望选择的卡牌上点击后,会转会战场,这个时候再点选要部署的位置即可
  • 不要在正在进行游戏的时候点右上角的叉退出。因为获取输入我使用的是 QEventLoop 这种方式来进行等待,如果此时关闭窗口会导致一些异常的结果,main 函数不能正常退出,程序会直接崩溃。