node.js利用socket.io实现多人在线匹配联机五子棋

5,404 阅读6分钟

项目地址,已上传github ——>

项目地址 -> 码云

项目演示地址 -> 欢迎大家来这里一起玩游戏哦~~

client端使用简单的h5+js实现了棋局的总体布局。 server端使用node的socket.io模块与客户端进行数据交互,棋子的落点和输赢校验均是在server端完成。
五子棋ui界面请见..

client端的界面这里就不做过多解释了,只要稍微懂点h5就可以自行去 这里下载源代码观看,因为今天的主题主要是socket.io这一块,所以本章只概述client和server是如何通过tcp连接进行交互的。

首先先带大家看一下目录结构

| server.js    (socket服务器)  
| gobang-ui.html   (是玩家下棋页面)  
| index.html  (是用户登陆界面)
| home.html   (是用户大厅界面, 用来匹配等待的 如果在线人数少于2人, 则匹配失败, 并会返回错误信息)
| game.html   (client端程序的入口,内嵌iframe来显示各个页面,通过改变iframe的src属性,来达成伪页面跳转)
| img   (图片资源文件夹)
    | tou.jpg   (棋盘界面用户的头像,因为登录界面只要输入用户名就可以开始游戏了,所以所有用户的头像都是一样的)

game.html主界面

    <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      width: 100%;
      height: 100%;
    }
  </style>
  <!-- 引入cdn上的socket.io库 -->
  <script src="https://cdn.bootcss.com/socket.io/2.1.0/socket.io.js"></script>
</head>
<body>
<!-- 这里是程序的入口,通过js改变src属性,来切换页面 -->
<iframe id="game" src="index.html" width="100%" height="100%" scrolling="no"></iframe>
</body>
</html>

为什么我们要用嵌入iframe改变src属性的方式来制造页面跳转的现象?因为页面的每一次跳转或刷新都会使socket连接断开。就好像http中的request请求一样,页面每次所以我们应尽量避免页面跳转这个操作。

    // 这行代码表示client端对server端进行第一次连接
var socket = io('ws://localhost:3000')

在index.html也就是用户的登录界面

<!-- 这是用户登录的按钮 -->
<div onclick="login()">开始游戏</div>

当点击了这个按钮之后,它会触发js中的login方法,但这个方法并不会直接去连接server,因为socket连接在game.html中,所以目前来看,这个页面只是game.html的子页面,这个方法在判断input中的value是否为空后,立即通过全局对象parent调用的父页面(game.html)中的login方法

// index.html中的login方法
function login() {
    if (username.value === undefined || username.value === '') {
      return
    }
    // 调用父窗口的login方法
    parent.login(username.value)
}

game.html中的login方法,这个方法通过socket向server触发了login事件

function login(username) {
    socket.emit('login', username)
}

server.js

// 监听连接
io.on('connection', function (socket) {
  // 玩家登陆, socket.emit('login', username)就是触发了这个事件
  // 监听了login事件
  socket.on('login', function (name) {
    // players是一个全局数组,里面存放了所有的玩家对象,如果players中 
    var flag = players.some(function (value) {
      return value.name === name
    })
    if (flag) {
      socket.emit('home', {'flag': true})
    } else {
      console.log(name + '已登陆')
      // 创建玩家
      new Player(socket, name)
      // 将玩家放进数组中
      // players.push(player)
      // 如果用户名没有重名,那么触发client端的home事件
      socket.emit('home', {'playerCount': playerCount, 'name': name})
    }
  })
})

玩家client对home事件的监听

  // 玩家登陆成功
  socket.on('home', function (data) {
    if (data.flag) {
      game.contentWindow.flag.hidden = false
    } else {
      game.contentWindow.flag.hidden = true
      // 保存用户名和玩家在线人数到localStorage中
      localStorage.setItem('name', data.name)
      localStorage.setItem('playerCount', data.playerCount)
      // location.href = './home.html'
      game.src = 'home.html'
    }
  })

home.html玩家等待大厅, home.html和index.html长得基本一致,所以它也有一个按钮,匹配按钮,通过它来触发play事件

  // 玩家开始匹配
  this.socket.on('play', function () {
    // 如果空闲玩家总数大于或等于2,那么开始游戏
    if (playerCount >= 2) {
      self.pipei = true
      // 如果已经有人在开始匹配了,那么这个玩家就不需要走下面函数了,因为继续执行的话相当于再开一个棋局
      if (isExistFZ(self) > 0) {
        // 保持不动就好,房主会自动找到你的
        return
      }
      // 如果没有房主,那么这个玩家将成为房主
      self.fz = true
      // 可用的玩家数
      var player2 = null
      self.timer = setInterval(function () {
        console.log('正在匹配...')
        if (player2 = findPlayer(self)) {
          console.log('匹配成功')
          self.gamePlay = new Game(self, player2)
          player2.gamePlay = self.gamePlay
          clearInterval(self.timer)
        }
      }, 1000)
    } else {
      socket.emit('player less')
    }
  })

server.js中有两个类,一个是Player玩家类,另一个是Game棋局类,一个棋局对应两个玩家。

Player类的属性

  this.socket = socket  // socket对象,玩家通过它来监听数据
  this.name = name  // 玩家的名称
  this.color = null // 玩家棋子的颜色
  this.state = 0  // 0代表空闲, 1在游戏中
  this.pipei = false  // 是否在匹配
  this.gamePlay = null // 棋局对象
  this.flag = true  // 是否轮到这个玩家出棋
  this.fz = false // 是否是房主

Player类对象监听的事件

// 监听玩家是否退出游戏
  this.socket.on('disconnect', function () {
    // 删除数组中的玩家
    // players.splice(players.indexOf(self), 1)  // 删不掉
    // delete players[players.indexOf(self)]
    // 新的删除方式
    players = players.filter(function (value) {
      return value.name !== self.name
    })
    playerCount--
    // 如果退出游戏的玩家正在进行游戏,那么这局游戏也该退出
    if (self.state === 0) {
      gameCount--
    }
    console.log(self.name + '已退出游戏')
  })
  
  // 玩家开始匹配
  this.socket.on('play', function () {
    // 如果空闲玩家总数大于或等于2,那么开始游戏
    if (playerCount >= 2) {
      self.pipei = true
      // 如果已经有人在开始匹配了,那么这个玩家就不需要走下面函数了,因为继续执行的话相当于再开一个棋局
      if (isExistFZ(self) > 0) {
        // 保持不动就好,房主会自动找到你的
        return
      }
      // 如果没有房主,那么这个玩家将成为房主
      self.fz = true
      // 可用的玩家数
      var player2 = null
      self.timer = setInterval(function () {
        console.log('正在匹配...')
        if (player2 = findPlayer(self)) {
          console.log('匹配成功')
          self.gamePlay = new Game(self, player2)
          player2.gamePlay = self.gamePlay
          clearInterval(self.timer)
        }
      }, 1000)
    } else {
      socket.emit('player less')
    }
  })
  
  // 玩家取消匹配按钮
  this.socket.on('clearPlay', function () {
    clearInterval(self.timer)
  })
  
   // 监听数据,玩家下棋的时候触发
  this.socket.on('data', function (data) {
    if (self.flag) {
      add_pieces(self.gamePlay, data, self.color)
    }
  })
  
  // 最后将当前玩家实例放到players全局玩家数组中去
  players.push(this)

Game(棋局类)

    // 棋盘的格子数
    this.column = 21
    this.arr = init_arr() // 存储棋盘坐标的二位数组
    
    // 一局棋局上的两个玩家
    this.play1 = play1
    this.play2 = play2
    // 修改游戏状态
      this.play1.state = 1
      this.play2.state = 1
      // 在游戏中,是否匹配为false
      this.play1.pipei = false
      this.play2.pipei = false
    
      this.play1.fz = false
      this.play1.fz = false
    
      // 随机给两个玩家分配棋子颜色
      this.play1.color = ~~(Math.random() * 2) === 0 ? 'white' : 'black'
      this.play2.color = this.play1.color === 'white' ? 'black' : 'white'
      // 谁是白棋谁先走
      this.play1.flag = this.play1.color === 'white'? true: false
      this.play2.flag = this.play2.color === 'white'? true: false

添加棋子方法

// 添加棋子
function add_pieces(self, position, color) {
  if (self.arr[position.x][position.y] === undefined) {
    self.arr[position.x][position.y] = color
    if (color === self.play1.color) {
      self.play1.flag = false
      self.play2.flag = true
    } else if (color === self.play2.color) {
      self.play1.flag = true
      self.play2.flag = false
    }
    check_result(self, self.arr, position, color)
  }
}

// 初始化数组
function init_arr() {
  var arr = []
  for (var i = 0; i < 21; i++) {
    arr.push(new Array(21))
  }
  return arr
}

如果大家喜欢的话,请在github上下载我的源码,谢谢大家支持!