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

zcimg2 年前
00

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

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

两人在线下棋演示

先动手玩一玩:https://gobang.zc-app.cn,可以用电脑和手机自己跟自己玩。
URL后面加/z就进入开发模式:https://gobang.zc-app.cn/z

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

棋盘结构

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

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

方阵结构

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

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

棋手状态

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

打开连接socket:ready

$socket.open("x", $c.exp, { login: 0, online: true, online: true})

第一个参数channel放你需关注的频道,我们随便给个简单的名字。
第二个参数$c.exp是个对象,可以包含onConn, onData, onError。

第三个参数是option选项,online表示有人上线时是否要通知到你,offline则是有人下线是要否通知,login为0时表示无需登录即可在多个设备中使用。

连上后:onConn

$socket.online("x")
$v.online = $r.session.filter('$x != $session')
render()

连上后查询一下关注"x"频道的在线玩家,排除自己(自己的会话是$session)后放到$v.online列表,然后向整个“x”频道发个消息询问各个在线玩家的名字。

消息格式

收到的消息都有共同的格式:type是消息类型,x是消息体,from是消息发送人,session是消息发送人的会话。消息接受人to和发送时间d在此案例中未使用到。

询问在线玩家的名字:askName

$socket.send("x", "askName")

回复自己的名字:getName

$socket.send(session, "getName", localStorage("name"))

把在ready时弹窗设置到localStorage的名字取出来回复askName的请求,并让对方执行getName的操作。

收到玩家名字的通知

$v.name[session] = x

有人上线了:online

$socket.send(session, "askName")
$v.online.push(session)
$v.online = $v.online.unique()

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

有人断线了:offline

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

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

收到数据后:onData

stopIf($session == session)
$c.exp[type].exc()
render()

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

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

发出对弈邀请

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

收到消息:on被邀

stopIf($v.状态, '$socket.send(from, "on拒邀", "对方正在下棋")')
$v.对手 = session
$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.方阵[$p.$i][$i] || $v.连续棋子.length > 4)
$v.落子点 = [$p.$i, $i]
$socket.send($v.对手, "on落子", $v.落子点)
$("." + $v.己方 + "子声音").play()
$v.方阵[$p.$i][$i] = $v.己方
$v.检查方向.forEach($c.exp.落_是否胜出)
$v.状态 = "对方出棋"

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

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

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

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

要判断胜负只需落子时从落子点 [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] === $p.$p.$i && $x[1] === $p.$i') ? "光晕" : ""

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

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

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

投诉
收藏
点赞 0
评论 0
由众触低代码平台生成和驱动