从零开始完整开发基于websocket的在线对弈游戏【五子棋】,只用几十行代码完成全部逻辑。

3,189 阅读5分钟

五子棋是规则简单明了的策略型游戏,先形成五子连线者获胜。
本课程习作采用两人在线对弈的方式进行比赛,拿着手机在上下班路上玩特别合适。

整个过程在众触低代码应用平台进行,使用表达式描述游戏逻辑(高度简化版JS)。
本课程重点学习websocket实时消息的发送与接收处理。

两人在线下棋演示

先动手玩一玩:gobang.zc-app.cn
因为是在线游戏,需要登录,可以用手机和邮箱分别注册,用电脑和手机自己跟自己玩。
URL后面加/z就进入开发模式:gobang.zc-app.cn/z

详尽的的教学请移步哔哩哔哩视频:www.bilibili.com/video/BV1QX…

棋盘结构

$v.棋盘 = array(14, array(14, ""))

用嵌套数据组件使用14 * 14的二维数组渲染而成,即图中画黑线的格子。

方阵结构

$v.方阵 = array(15, array(15, ""))

用嵌套数据组件使用15 * 15的二维数组渲染而成,即图中黑线交叉的点,鼠标hover在组件上时高亮的圆圈。

棋手状态

undefined、邀请中、对方出棋、己方出棋
默认就是没有值,表示还没开始下棋或者结束了(胜负已分)。对方出棋的时候己方不能落子。

账号登录

既然是在线玩的,就要求登录。可以用手机或邮箱注册。
登录后马上打开连接,有玩家上线了,有玩家邀请了,对方落子了都是通过此连接socket即时通知的。

打开连接socket:onLogin

$socket.open($c.exp, { channels: ["比赛"], onOnline: true, onOffline: true, allowMultiLogin: true })

第一个参数$c.exp是个对象,可以包含onConnect, onData, onReconnect, onError,前两个是必须的。

第二个参数是option选项,channels数组里放你需关注的频道,onOnline表示有人上线时是否要通知到你,onOffline则是有人下线是要否通知,allowMultiLogin表示同一个账号能否能在多个地方登录而不强制前面登录的账号下线。

连上后:onConnect

$socket.onlines(["比赛"])
$v.onlines = $r.比赛.filter('$x !== $c.me._id')
$v.onlines.forEach('$user.get($x)')
render()

连上后查询一下关注"比赛"频道的在线玩家,排除自己后放到$v.onlines列表后依次获取用户信息。

消息格式

下面有多个on开头的表达式都是只收到socket消息。它们都有共同的格式:type是消息类型,x是消息体,from是消息发送人。消息接受人to和发送时间d在此案例中未使用到。

有人上线了:onOnline

$v.onlines.push(x)
$v.onlines = $v.onlines.unique()
$user.get(x)

把上线的人放入上面的$v.onlines中,并去重。

有人断线了:onOffline

$v.对手 === x ? alert("对方断线了") : ""
$v.onlines.splice($v.onlines.indexOf(x), 1)

断线了就把他/她从$v.onlines移除。如果刚好是正在跟你对弈的棋手则抛出一个警告通知。

收到数据后:onData

stopIf($c.me._id == from)
$c.exp[type].exc()
render()

先要排除是自己发出的数据,因为socket是广播消息的,自己也能收到。
然后再根据消息类型执行对应的表达式,可能的类型有:on被邀、on拒邀、on受邀、on落子。

当其他人登录时,【对手】右边的问号圆圈就会闪烁,点击它会弹出在线玩家列表,从中选择一个可发出对弈邀请。

发出对弈邀请

$socket.send($x, "on被邀", "邀请")
$v.状态 = "邀请中"
info("邀请已发出,请等待对方接受邀请")

收到消息:on被邀

stopIf($v.状态, '$socket.send(from, "on拒邀", "对方正在下棋")')
$user.get(from)
$v.对手 = from
$v.pop = "选棋子"

如果自己正在下棋就直接发出"on拒邀"消息,拒绝邀请。
获取对方用户信息,弹出模态窗口提示接受要是拒绝邀请。

收到消息:on拒邀

$v.状态 = undefined
warn(x || "对方拒绝你的邀请")

把前面的”邀请中“的状态置空,弹出对方发来的拒邀消息

选子

$v.己方 = "白" // "黑"
$c.exp.受邀.exc()

接受邀请:受邀

$socket.send($v.对手, "on受邀", $v.己方)
$v.方阵 = array(15, array(15, ""))
$v.pop = undefined
$v.对方 = ($v.己方 === "黑" ? "白" : "黑")
$v.状态 = "己方出棋"
info("请出棋")

给对方发送“on被邀“消息,捎上自己选的子。
清空方阵,准备出棋。

收到消息:on被邀

$v.方阵 = array(15, array(15, ""))
$v.对手 = from
$v.对方 = x
$v.己方 = (x === "黑" ? "白" : "黑")
$v.状态 = "对方出棋"
info("对方已接受邀请,请等待对方先出棋")

from是对手用户ID,x是对方选的子,自己就只能选另一种子了。

落子

stopIf($v.状态 !== "己方出棋" || $v.方阵[$parent.$index][$index] || $v.连续棋子.length > 4)
$v.落子点 = [$parent.$index, $index]
$socket.send($v.对手, "on落子", $v.落子点)
$("." + $v.己方 + "子声音").play()
$v.方阵[$parent.$index][$index] = $v.己方
$v.检查方向.forEach($c.exp.落_是否胜出)
$v.状态 = "对方出棋"

如果不是己方出棋的状态,或者落子位置不在方阵内,或者已经组成4个以上连续棋子都不可落子。
发出"on落子"消息,捎上刚才的落子点坐标轴。
播放落子声音,并把己方棋子放在方阵的落子点上,并通过动态类名发出光晕。

$v.落子点[0] === $parent.$index && $v.落子点[1] === $index ? "光晕" : ""

检查刚才的落子能否胜出。

检查胜出(形成五子连线)

要判断胜负只需落子时从落子点 [y, x] 以四种连线的正反方向分别查看,累计4个以上连续同色棋子为声。

$v.检查方向

[    [        [-1, 0],
        [1, 0]
    ],
    [        [0, -1],
        [0, 1]
    ],
    [        [1, -1],
        [-1, 1]
    ],
    [        [-1, -1],
        [1, 1]
    ]
]

-1表示往后检查,0表示不动,1表示往前检查。比如[-1, 0]是是X轴上往负值方向检查,即正西方向;[1, -1]表示先往X轴正方向检查再往Y轴负方向检查,即东北方向。

落子是否胜出

$v.连续棋子 = [$v.落子点]
$l.方向 = $x[0]
$l.非连续 = false
$v.循环4次.forEach($c.exp.落_相邻同色)
$l.方向 = $x[1]
$l.非连续 = false
$v.循环4次.forEach($c.exp.落_相邻同色)
stopIf($v.连续棋子.length > 4, 'info(($v.状态 === "己方出棋" ? $v.己方 : $v.对方) + "子赢了"); $v.状态 = undefined;')

先把当前落子位置作为第一个连续棋子,先往$v.检查方向提供的一对方向的第一个方向试探移动4次(即循环4遍)看是否有相邻同色子,再往另一个方向也试探4次。
如果试探得到的$v.连续棋子大于4个,那当前落子方胜出。

检查与落子相邻的同色子

$l.y = $v.落子点[0] + $l.方向[0] * $x
$l.x = $v.落子点[1] + $l.方向[1] * $x
!$l.非连续 && $v.方阵[$l.y][$l.x] === ($v.状态 === "己方出棋" ? $v.己方 : $v.对方) ? $v.连续棋子.push([$l.y, $l.x]) : $l.非连续 = true

一个试探方向包括X轴方向和Y轴方向,有-1、0、1三种移法,分别移动一下坐标,检查新坐标在方阵中的棋子,如果坐标上有子,并且现在是己方出棋而且这个子正好是己方颜色,那这个子就是连续棋子的一部分。其它情况都不能算连续同色子,比如坐标上没有子,或者是对方的子,再或者是以前就已经非连续了,这次就没必要继续检查了。

胜出的连续5个棋子也要发出光晕。前面新落的子已经通过动态类名发出光晕,现在要找出连续棋子的其它棋子。

$v.连续棋子.length > 4 && $v.连续棋子.find('$x[0] === $ext.$parent.$index && $x[1] === $ext.$index') ? "光晕" : ""

我们从连续棋子里面找,看看里面是否有一个棋子的坐标跟当前检查的坐标位置相同。x\[0\]是连续棋子X坐标,x[1]是Y坐标。注意,这里是嵌套数据组件里作为动态类名的,index是当前数据组件的下标,index是当前数据组件的下标,parent.index是上一层数据组件的下标。但由于它们是放在find()函数里面的,需要在前面添加index是上一层数据组件的下标。但由于它们是放在find()函数里面的,需要在前面添加`ext.表示它们函数外面上下文提供的数据,如果没有$ext.`,那就成了find()函数提供给的上下文数据了。

准备深入研究的同学请到www.zcappp.cn/course/goba…页面后,点击右侧的【克隆】按钮,把整个游戏复制一份随意玩弄更改。

更多教学视频请移步哔哩哔哩空间:space.bilibili.com/475645807,里面不仅有各种前端可视化案例演示和讲解,还有多个完整功能的网站应用案例的开发过程演示和讲解。