前一篇文章:网页版五子棋——匹配模块(服务器端开发)-CSDN博客
项目源代码:Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com
目录
·前言
前面文章介绍完了五子棋项目中用户模块及匹配模块的代码编写,从本篇文章开始就进入五子棋项目最核心的模块—— “对战模块” ,在这个模块中要做的事情就是处理玩家之间的正常对弈,可以正确的判定对局的结果,在这个模块中会对前面模块的部分代码有所修改,本篇文章要介绍的内容是对战模块中前后端交互接口的设计,以及游戏房间页面的编写,还有棋盘信息的绘制,和前后端交互代码的编写,本篇文章中新增的代码文件如下图圈起来的部分所示:
下面开始本篇文章的内容介绍。
一、前后端交互接口设计
我们先来设计一下对战模块这里的前后端交互接口,在我们的对战模块中也是需要用到消息推送这样的机制的,因为我们需要让一方玩家进行落子操作后另一方可以直接立刻观察到,这就需要在玩家1 进行落子给服务器发送落子请求时,服务器要主动给进行游戏的双方玩家一个响应,具体就是体现在棋盘上新出现的棋子,这个过程仍然是使用 WebSocket 建立连接来使客户端与服务器进行通信,那么就需要设计一下前后端交互的接口,这样才能方便我们后面代码的编写,前后端接口设计如下所示:
- 对战连接:
ws://127.0.0.1:8080/game
在匹配模块中已经建立了一个 WebSocket 连接路径,但是由于对战模块和匹配模块使用的是两套逻辑,为了做到更好的解耦合,这里我们使用不同的 WebSocket 的连接路径进行处理。
- 连接响应:
{
message: 'gameReady', // 消息类别:游戏准备就绪
ok: true, // 连接是否成功
reason: '', // 错误原因
roomId: '1234a', // 玩家对弈所处房间的房间号
thisUserId: 1, // 玩家自己的 id
thatUserId: 2, // 对手的 id
whiteUser: 1 // 先手方的 id
}
当两个玩家都连接好了,服务器要生成一些游戏的初始信息,通过这个响应来告诉客户端,同时也可以告知客户端玩家双方都准备就绪了。
- 落子请求:
{
message: 'putChess', // 消息类别:落子
userId: 1, // 落子玩家的 id
row: 0, // 落子的行数
col: 0 // 落子的列数
}
- 落子响应:
{
message:'putChess', // 消息类别:落子
userId: 1, // 接收响应的玩家 id
row: 0, // 落子的行数
col: 0, // 落子的列数
winner: 0 // 当前是否分出胜负
}
响应中,如果 winner 为 0 ,表示未分出胜负,还有继续往下对战,如果 winner 非 0 ,则表示当前获胜方玩家的 id。
接口设计完成,下面开始页面代码的编写。
二、游戏房间页面基本结构
我们要编写的游戏房间页面的大致轮廓如下图所示:
1.游戏房间页面布局
根据上面大致轮廓图,我们来创建 game_room.html 来表示对战的页面,这里包含的内容如下:
- 引入了 canvas 标签,这是 HTML5 引入的 “画布”,后面我们实现棋盘和棋子的绘制就要依赖这个画布的功能。
- #screen 用于显示当前的对局状态,比如:“等待玩家连接中……”,“轮到你落子了!”,“轮到对方落子了!”等。
game_room.html 代码及详细介绍如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>游戏房间</title>
<!-- 引入 css 样式 -->
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/game_room.css">
</head>
<body>
<div class="nav">五子棋对战</div>
<div class="container">
<div>
<!-- 棋盘区域, 基于 canvas 进行实现 -->
<canvas id="chess" width="450px" height="450px"></canvas>
<!-- 显示区域 -->
<div id="screen"> 等待玩家连线中.... </div>
</div>
</div>
</body>
</html>
2.游戏房间页面样式设计
编写完 game_room.html 页面代码后,我们来为这个页面编写一些样式代码,使页面看起来更美观一些,在我画的轮廓图中,导航栏及页面主体区域的样式在前面已经编写过了,存放在了公共样式设置的 common.css 代码中,所以可以直接引入,下面我们创建 game_room.css 文件,来编写游戏房间页面的样式,代码及详细介绍如下所示:
/* 游戏房间样式 */
/* 设置显示信息区域的样式 */
#screen {
width: 450px;
height: 50px;
margin-top: 10px;
background-color: #fff;
font-size: 22px;
line-height: 50px;
text-align: center;
}
/* 设置 "返回大厅" 按钮的样式 */
button {
width: 450px;
height: 50px;
/* 设置字体样式 */
font-size: 20px;
color: white;
/* 设置背景颜色 */
background-color: orange;
/* 去除边框与轮廓线 */
border: none;
outline: none;
/* 把边框的棱角钝化 */
border-radius: 10px;
/* 设置文字位置 */
text-align: center;
/* 使文字垂直居中 */
line-height: 50px;
/* 增加与上面 div 的间距 */
margin-top: 10px;
}
/* 设置按钮点击后的样式 */
button:active {
background-color: gray;
}
编写完成这些代码后,游戏房间页面的基本结构也就完成了,下面我们来看看页面效果,如下图所示: 如上图所示,我们游戏房间页面基本结构就是这样,由于还没有进行棋盘信息的绘制,所以目前还无法直接观察到棋盘区域,但是我们使用浏览器的检查功能还是可以看出棋盘的位置,下面就来进行棋盘信息的绘制。
三、实现棋盘信息绘制
此处棋盘和棋子都是基于 canvas API 画在 canvas 标签框选区域的画布上的,下面我们来创建 script.js,在这个代码文件中,我们主要实现棋盘的绘制,前后端交互代码的编写,处理页面落子的逻辑,关于这个代码文件其中包含的内容如下:
- 这里棋盘与棋子绘制的代码基于 canvas API 进行编写;
- 使用一个二维数组来表示棋盘,虽然胜负的逻辑我们是在服务器来进行判定,但是在我们客户端这里要保证画的棋子要避免 “一个位置重复落子” 的情况;
- onStep 函数:作用是在一个指定的位置上绘制一个棋子,可以区分出绘制的是白子还是黑子,参数是横坐标和纵坐标,分别对应列和行;
- 用 onclick 来处理用户的点击事件,当用户点击棋盘的某个位置时,通过这个函数来控制在哪里绘制棋子;
- 使用 me 这个变量来表示当前是否轮到我落子,over 变量来表示游戏结束;
- 棋盘要有一个背景图,这个背景图要放在 image 目录下。
介绍完 script.js 基本的内容后,我们来编写 script.js 的代码,具体代码及详细介绍如下:
// 表示对局的初始化信息
let gameInfo = {
roomId: null,
thisUserId: null,
thatUserId: null,
isWhite: true,
}
// 设定界面显示相关操作
function setScreenText(me) {
let screen = document.querySelector('#screen');
// 判断当前是谁落子
if (me) {
screen.innerHTML = "轮到你落子了!";
} else {
screen.innerHTML = "轮到对方落子了!";
}
}
// 初始化一局游戏
function initGame() {
// 是我下还是对方下. 根据服务器分配的先后手情况决定
let me = gameInfo.isWhite;
// 游戏是否结束
let over = false;
let chessBoard = [];
//初始化chessBord数组(表示棋盘的数组)
for (let i = 0; i < 15; i++) {
chessBoard[i] = [];
for (let j = 0; j < 15; j++) {
chessBoard[i][j] = 0;
}
}
let chess = document.querySelector('#chess');
let context = chess.getContext('2d');
context.strokeStyle = "#BFBFBF";
// 背景图片
let logo = new Image();
logo.src = "image/棋盘背景.png";
logo.onload = function () {
context.drawImage(logo, 0, 0, 450, 450);
initChessBoard();
}
// 绘制棋盘网格
function initChessBoard() {
for (let i = 0; i < 15; i++) {
context.moveTo(15 + i * 30, 15);
context.lineTo(15 + i * 30, 430);
context.stroke();
context.moveTo(15, 15 + i * 30);
context.lineTo(435, 15 + i * 30);
context.stroke();
}
}
// 绘制一个棋子, me 为 true
function oneStep(i, j, isWhite) {
context.beginPath();
// 画一个弧线
context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
context.closePath();
var gradient = context.createRadialGradient(15 + i * 30 + 2, 15 + j * 30 - 2, 13, 15 + i * 30 + 2, 15 + j * 30 - 2, 0);
// 根据 isWhite 来判断画的是黑子还是白子
if (!isWhite) {
gradient.addColorStop(0, "#0A0A0A");
gradient.addColorStop(1, "#636766");
} else {
gradient.addColorStop(0, "#D1D1D1");
gradient.addColorStop(1, "#F9F9F9");
}
context.fillStyle = gradient;
context.fill();
}
// 在棋盘点击落子, 就会触发这个方法
chess.onclick = function (e) {
// 如果点击操作超出范围, 或者当前不是我落子的回合
// 就不会进行任何操作
if (over) {
return;
}
if (!me) {
return;
}
// 设置落子的位置
let x = e.offsetX;
let y = e.offsetY;
// 注意, 横坐标是列, 纵坐标是行
// 这里做一个精确落子位置的操作, 使玩家点击落子后即使没有正好
// 点到两线交点也可以很好的确定落子位置
let col = Math.floor(x / 30);
let row = Math.floor(y / 30);
// 判断当前位置是否有棋子
if (chessBoard[row][col] == 0) {
oneStep(col, row, gameInfo.isWhite);
chessBoard[row][col] = 1;
}
}
}
initGame();
编写完 script.js 的基础代码后,我们再来运行一下程序,观察一下页面的变化,如下图所示:
此时棋盘信息的绘制就完成了,并且当点击棋盘某个位置时,棋子也可以显示出来。
四、前后端交互代码编写
编写完上面的代码后,客户端页面的显示就没有什么问题了,下面我们需要让服务器能感知到我们玩家已经准备就绪,并且可以感知到我们玩家在何处进行落子,这就需要我们编写前后端交互的代码,把需要服务器感知的操作都以请求的方式发送给服务器。
1.初始化 WebSocket
给服务器发送请求和接收服务器返回的响应,需要建立连接,根据前面的介绍,我们这里需要建立的是 WebSocket 连接,因为我们这里需要用到消息推送的机制,下面我们就来初始化 WebSocket ,这里我们需要做以下的事情:
- 在 script.js 代码中,加入 WebSocket 的连接代码,实现前后端交互;
- 删掉原来的 initGame 函数的调用,这里的初始化棋盘的操作要在获取到服务器返回的响应之后再进行初始化棋盘;
- 创建 WebSocket 对象,并实现 onopen、onclose、onerror、onbeforeunload 方法;
- 实现 onmessage 方法,在这个方法中处理要先游戏就绪的响应。
介绍完这里初始化 WebSocket 要做的事情,下面我们就可以编写代码了,具体的代码及详细介绍如下所示:
// 初始化 websocket
let websocketUrl = 'ws://' + location.host + '/game';
let websocket = new WebSocket(websocketUrl);
// 连接成功建立的回调方法
websocket.onopen = function() {
console.log("连接房间成功!");
}
// 连接关闭的回调方法
websocket.onclose = function() {
console.log("和游戏服务器断开连接!");
}
// 连接发生错误的回调方法
websocket.onerror = function() {
console.log("和服务器的连接出现异常!");
alert('和服务器连接断开! 返回游戏⼤厅!');
location.assign('/game_hall.html');
}
// 监听页面关闭事件,在页面关闭之前,手动调用这里的 websocket 的 close 方法.
window.onbeforeunload = function() {
websocket.close();
}
// 处理服务器返回的响应数据
websocket.onmessage = function(event) {
console.log("[handlerGameReady] " + event.data);
// JSON.parse 把 JSON 字符串转换成 JS 对象
let resp = JSON.parse(event.data);
if (!resp.ok) {
alert("连接游戏失败! reason: " + resp.reason);
// 如果出现连接失败的情况, 回到游戏大厅
location.assign("/game_hall.html");
return;
}
// 如果准备就绪,就进行对局的初始化
if (resp.message == 'gameReady') {
// 初始化对局信息
gameInfo.roomId = resp.roomId;
gameInfo.thisUserId = resp.thisUserId;
gameInfo.thatUserId = resp.thatUserId;
gameInfo.isWhite = (resp.whiteUser == resp.thisUserId);
// 初始化棋盘
initGame();
// 初始化显示区域的内容
setScreenText(gameInfo.isWhite);
} else if (resp.message == 'repeatConnection') {
alert("当前检测到多开! 请使用其他账号进行登录! ");
location.assign("/login.html");
}
}
2.发送落子请求
在上面 script.js 代码中,用 onclick 函数来处理玩家点击棋盘时的绘制棋子的操作,我们现在想让玩家在点击棋盘触发 onclick 函数时不进行绘制棋子操作了,而是给服务器发送一个落子的请求,告知服务器要在哪里落子,让服务器判断落子后对局中有没有出现“五子连珠”的情况,在返回的响应中进行棋子的绘制,下面我们就来修改 onclick 函数,这里我们要做以下的事情:
- 在落子操作时加入发送请求的逻辑;
- 删除原有的 onStep 和修改 chessBoard 的操作,放到接收落子响应时处理;
- 编写 send 函数,通过 WebSocket 发送落子请求。
介绍完如何修改 onclick 函数后,我们就可以开始进行修改代码了,修改后 onclick 函数具体的代码及详细介绍如下:
// 在棋盘点击落子, 就会触发这个方法
chess.onclick = function (e) {
// 如果点击操作超出范围, 或者当前不是我落子的回合
// 就不会进行任何操作
if (over) {
return;
}
if (!me) {
return;
}
// 设置落子的位置
let x = e.offsetX;
let y = e.offsetY;
// 注意, 横坐标是列, 纵坐标是行
// 这里做一个精确落子位置的操作, 使玩家点击落子后即使没有正好
// 点到两线交点也可以很好的确定落子位置
let col = Math.floor(x / 30);
let row = Math.floor(y / 30);
// 判断当前位置是否有棋子
if (chessBoard[row][col] == 0) {
// 发送坐标给服务器, 服务器要返回结果
send(row, col);
}
}
// 发送落子的请求
function send(row, col) {
// 根据前后端交互接口构造请求数据
let req = {
message: 'putChess',
userId: gameInfo.thisUserId,
row: row,
col: col
};
// 使用 websocket 连接发送请求
// 用 JSON.stringify 把请求构造成 JSON 字符串
websocket.send(JSON.stringify(req))
}
3.处理落子响应
发送完落子的请求之后,服务器要在内部先判断有没有出现“五子连珠”的情况,然后把落子的响应返回给客户端,让客户端在对应的位置落子,同时显示对局的信息,下面我们要在 initGame 中,修改 websocket 的 onmessage 方法,这里我们要做以下的事情:
- 在调用 initGame 方法之前,websocket 的 onmessage 方法就是处理游戏就绪的响应,在收到游戏就绪响应后,就要改为接收落子的响应了;
- 在处理落子响应时要处理棋子的绘制、交换双方落子的轮次、更改显示框的显示信息、判断游戏是否结束。
介绍完要如何修改 websocket 的 onmessage 方法后,我们就可以进行修改操作了,这里虽然说是修改,其实是在 initGame 方法中添加一个 websocket 的message 方法,修改后的代码及详细介绍如下:
// 之前 websocket.onmessage 主要是用来处理游戏就绪的响应, 在游戏就绪之后, 初始化完毕之 后就不再有游戏就绪响应了
// 所以就在这个 initGame 内部, 修改 websocket.onmessage 方法, 让这个方法里面针对落子
websocket.onmessage = function(event) {
console.log("[handlerGameReady] " + event.data);
// JSON.parse 把 JSON 字符串转换成 JS 对象
let resp = JSON.parse(event.data);
if (resp.message != "putChess") {
console.log("响应类型错误!");
return;
}
// 先判定当前这个响应是自己落的子, 还是对方落的子
if (resp.userId == gameInfo.thisUserId) {
// 我自己落的子
// 根据我自己子的颜色, 来绘制一个棋子
oneStep(resp.col, resp.row, gameInfo.isWhite);
} else if (resp.userId == gameInfo.thatUserId) {
// 对手落的子
oneStep(resp.col, resp.row, !gameInfo.isWhite);
} else {
// 响应错误! userId 有问题!
console.log('[handlerPutChess] resp userId 错误!');
return;
}
// 给对应的位置设为 1, 方便后续逻辑判定当前位置是否已经有子了
chessBoard[resp.row][resp.col] = 1;
// 交换双方的落子轮次
me = !me;
// 更改显示框的显示信息
setScreenText(me);
// 判定游戏是否结束
let screenDiv = document.querySelector('#screen');
if (resp.winner != 0) {
// 根据玩家 Id 来判断是哪方获胜
if (resp.winner == gameInfo.thisUserId) {
screenDiv.innerHTML = '你赢了!';
} else if (resp.winner == gameInfo.thatUserId) {
screenDiv.innerHTML = '你输了!';
} else {
alert("winner 字段错误! " + resp.winner);
}
// 增加一个按钮, 让玩家点击之后, 再回到游戏大厅
let backButton = document.createElement('button');
backButton.innerHTML = '回到大厅';
backButton.onclick = function() {
// 防止浏览器回退造成不正确的结果
// 这里使用 replace 进行跳转, 跳转之后就无法回退到当前页面了
location.replace('/game_hall.html');
}
// 在 container div 下拼接按钮
let fatherDiv = document.querySelector('.container>div');
fatherDiv.appendChild(backButton);
}
}
·结尾
文章到此就要结束了,本篇文章主要介绍了对战模块中用到的前后端交互接口的设计,并编写了游戏房间页面的代码,完成了棋盘信息的绘制及前后端交互的代码,这里由于没有对服务器端代码进行编写,所以还不能进行效果的演示,但是五子棋项目的对战模块客户端的代码到这里也是基本完成了,如果对本篇文章的内容有所疑惑,欢迎在评论区进行留言,如果感觉本篇文章还不错,希望能收到你的三连支持,下一篇文章就开始五子棋项目对战模块中的服务器端代码的开发了,我们下一篇文章再见吧~~~
标签:落子,resp,页面,五子棋,对战,let,棋盘,代码,客户端 From: https://blog.csdn.net/HKJ_numb1/article/details/143689067