游戏时间限制

不知道童鞋们有没有发现目前游戏有一个天大的Bug:在目前的游戏机制下,我们的躲藏者永远都是赢不了的,只有被追着跑的份,这样做出来的游戏平衡性太差了。

这一章我们将要为躲藏者讨回一个公道,增加每局的游戏时间限制,让躲藏者也有赢的机会。

既然要做时间限制,那就需要用到定时器功能了,Swoole框架为我们提供了两种定时器:循环定时器swoole_timer_tick和一次性定时器swoole_timer_after。至于两种定时器有什么区别童鞋们可以参考官方文档,或者参考附录二:Swoole入门篇(下),我们的需求是游戏时间的限制,很显然在这里需使用的是swoole_timer_after

功能的逻辑思想很简单,当游戏开始的时候,创建一个定时器,当时间到了则判定躲藏者胜利,在流程上并不会影响到目前的游戏初始化流程。

做题时间

  1. 游戏初始化的时候,使用swoole_timer_after创建一个定时器,定时10秒钟。
  2. 发送时间秒数给前端,前端进行倒计时渲染。
  3. 当触发定时器的时候,判断房间是否存在,若存在则判定躲藏者胜利,并关闭游戏房间。
  4. 前端增加当前玩家类型展示。

index.html

  1. ...
  2. <div id="app">
  3. ...
  4. <div v-if="roomId">
  5. <div>
  6. 房间号:{{roomId}}
  7. </div>
  8. <div v-if="timeLimit">
  9. 剩余时间:{{timeLimit}}
  10. </div>
  11. </div>
  12. ...
  13. <div v-if="playerType">
  14. 本局玩家类型:{{playerTypeArr[playerType]}}
  15. </div>
  16. ...
  17. </div>
  18. <script>
  19. var app = new Vue({
  20. ...
  21. data: {
  22. ...
  23. playerType: null,
  24. playerTypeArr: [0, '寻找者', '躲藏者'],
  25. timeLimit: null,
  26. timerId: null
  27. },
  28. ...
  29. methods: {
  30. ...
  31. websocketonmessage(e) { //数据接收
  32. ...
  33. switch (message.code) {
  34. ...
  35. case 1004://游戏数据
  36. this.mapData = responseData.map_data;
  37. if (!this.playerType){
  38. let players = responseData.players;
  39. this.playerType = players[this.playerId].type
  40. }
  41. if (!this.timerId) {
  42. this.timeLimit = responseData.time_limit
  43. var that = this
  44. this.timerId = setInterval(function () {
  45. that.timeLimit--
  46. }, 1000);
  47. }
  48. break;
  49. ...
  50. }
  51. },
  52. ...
  53. }
  54. })
  55. </script>
  56. ...

Logic类:

  1. <?php
  2. ...
  3. class Logic
  4. {
  5. ...
  6. const GAME_TIME_LIMIT = 10;
  7. ...
  8. public function startRoom($roomId, $playerId)
  9. {
  10. ...
  11. if (empty(count($gameManager->getPlayers()))) {
  12. ...
  13. } else {
  14. //第二个玩家
  15. $gameManager->createPlayer($playerId, 6, 10);
  16. DataCenter::$global['rooms'][$roomId]['timer_id'] = $this->createGameTimer($roomId);
  17. Sender::sendMessage($playerId, Sender::MSG_ROOM_START);
  18. $this->sendGameInfo($roomId);
  19. }
  20. }
  21. ...
  22. private function sendGameInfo($roomId)
  23. {
  24. ...
  25. foreach ($players as $player) {
  26. $data = [
  27. 'players' => $players,
  28. 'map_data' => $this->getNearMap($mapData, $player->getX(), $player->getY()),
  29. 'time_limit' => self::GAME_TIME_LIMIT
  30. ];
  31. Sender::sendMessage($player->getId(), Sender::MSG_GAME_INFO, $data);
  32. }
  33. }
  34. ...
  35. private function createGameTimer($roomId)
  36. {
  37. return swoole_timer_after(self::GAME_TIME_LIMIT * 1000, function () use ($roomId) {
  38. if (isset(DataCenter::$global['rooms'][$roomId])) {
  39. //游戏还未结束则主动结束游戏
  40. /**
  41. * @var Game $gameManager
  42. */
  43. $gameManager = DataCenter::$global['rooms'][$roomId]['manager'];
  44. $players = $gameManager->getPlayers();
  45. $winner = end($players)->getId();
  46. DataCenter::addPlayerWinTimes($winner);
  47. foreach ($players as $player) {
  48. Sender::sendMessage($player->getId(), Sender::MSG_GAME_OVER, ['winner' => $winner]);
  49. DataCenter::delPlayerRoomId($player->getId());
  50. }
  51. unset(DataCenter::$global['rooms'][$roomId]);
  52. }
  53. });
  54. }
  55. }

createGameTimer()方法里创建的定时器回调方法其实与原有的checkGameOver()方法逻辑非常类似,对于相同的代码我们应该要抽出来作为一个方法调用。

Logic类:

  1. <?php
  2. ...
  3. class Logic
  4. {
  5. ...
  6. private function checkGameOver($roomId)
  7. {
  8. /**
  9. * @var Game $gameManager
  10. * @var Player $player
  11. */
  12. $gameManager = DataCenter::$global['rooms'][$roomId]['manager'];
  13. if ($gameManager->isGameOver()) {
  14. $players = $gameManager->getPlayers();
  15. $winner = current($players)->getId();
  16. $this->gameOver($roomId, $winner);
  17. }
  18. }
  19. private function createGameTimer($roomId)
  20. {
  21. return swoole_timer_after(self::GAME_TIME_LIMIT * 1000, function () use ($roomId) {
  22. if (isset(DataCenter::$global['rooms'][$roomId])) {
  23. //游戏还未结束则主动结束游戏
  24. /**
  25. * @var Game $gameManager
  26. */
  27. $gameManager = DataCenter::$global['rooms'][$roomId]['manager'];
  28. $players = $gameManager->getPlayers();
  29. $winner = end($players)->getId();
  30. $this->gameOver($roomId, $winner);
  31. }
  32. });
  33. }
  34. private function gameOver($roomId, $winner)
  35. {
  36. /**
  37. * @var Game $gameManager
  38. * @var Player $player
  39. */
  40. $gameManager = DataCenter::$global['rooms'][$roomId]['manager'];
  41. $players = $gameManager->getPlayers();
  42. DataCenter::addPlayerWinTimes($winner);
  43. foreach ($players as $player) {
  44. Sender::sendMessage($player->getId(), Sender::MSG_GAME_OVER, ['winner' => $winner]);
  45. DataCenter::delPlayerRoomId($player->getId());
  46. }
  47. unset(DataCenter::$global['rooms'][$roomId]);
  48. }
  49. }

为实体类Player实现JsonSerializable接口,满足sendGameInfo()方法中输出$players对象的需求。

Player类:

  1. <?php
  2. ...
  3. class Player implements \JsonSerializable
  4. {
  5. ...
  6. public function jsonSerialize()
  7. {
  8. return [
  9. 'id' => $this->id,
  10. 'type' => $this->type,
  11. 'x' => $this->x,
  12. 'y' => $this->y,
  13. ];
  14. }
  15. }

代码写完了,我们赶紧来测试一下游戏效果:

17 游戏时间限制 - 图1

可以看到,左上角已经成功渲染出“剩余时间:6”的文字,也成功展示出了玩家类型

17 游戏时间限制 - 图2

当触发计时器的时候,躲藏者获得了胜利,游戏功能正常。

Homework

本章就到这里结束了,相信童鞋们已经大概掌握了定时器的用法,但其实我们目前的游戏逻辑还是不够严谨的。

如:

  • 当定时器触发的同一时间,寻找者抓到了躲藏者该怎么处理?

这个问题就留给童鞋们自己去思考了,一种比较容易的解决方法就是增加锁机制。

本章对应Github Commit扩展七:游戏时间限制