项目地址, 已上传 github --> https://github.com/dagger9527/gobang-online
client 端使用简单的 h5+js 实现了棋局的总体布局. server 端使用 node 的 socket.io 模块与客户端进行数据交互, 棋子的落点和输赢校验均是在 server 端完成.
五子棋 ui 界面请见.. https://github.com/dagger9527/gobang-ui
client 端的界面这里就不做过多解释了, 只要稍微懂点 h5 就可以自行去 这里 https://github.com/dagger9527/gobang-ui 下载源代码观看, 因为今天的主题主要是 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 上下载我的源码, 谢谢大家支持!
来源: https://juejin.im/post/5b0f5f4a6fb9a00a202cb5a4