自从开始接触Swing以来,就喜欢写写各种管理系统,写多了就萌生了一种类似于实时在线对战的游戏,经过一番构思后就开始着手设计这个网络对战版本的五子棋了。
游戏代码包含两部分,常规的C/S模式(C代表客户端,S代表服务端)
下载代码后先启动服务器,服务器正常启动后,你会在控制台看到相关的日志(这里注意,服务器是没有做界面管理的),接着启动客户端(可以启动多个客户端),连接服务器后点击菜单栏联网、对战、匹配等操作

游戏效果图:

欢迎大家支持新自的研游戏:中国象棋

下载在线客户端版本试玩:

链接:https://pan.baidu.com/s/1-Bt8tcuZGkVj-jFxN4YDbg 密码:w2oy
要求:jdk环境 1.6或以上
使用方式:环境正常安装后,解压下载的文件,点击 startClient.bat 就可以打开了
备注:若你只开一个客户端,进行匹配的话可能没人跟你玩,建议不是为了技术纯测试的话与你和你的朋友一起对战

阅读本文前,您需要了解:

  • java swing(好像是废话)
  • socket
  • json
  • 多线程(不多,计时用了一下)

1:服务端与客户端数据交互如何约定?

在c/s程序的设计之初,如何按照约定的方式进行数据交互一直是一个需要解决的问题,在我这个程序中,有一个常量类定义如下,这是我与客户端进行的一个约定,任何请求都会含有一个基础数据(key),而所做的工作就是这个基础数据(key)所对应的基础数据(value)
package cn.xt.net;import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;public class Const {public static final int PORT = 7001; // 监听端口// 基础数据(key)public static final String ID = "id";public static final String MSG = "msg";// 基础数据(value)public static final String ID_STATUS_ERROR = "idError"; // 错误信息public static final String ID_STATUS_INIT = "初始化客户端"; // 向服务器请求初始化public static final String ID_STATUS_PP = "匹配玩家";public static final String PP_SUCCESS = "匹配成功";public static final String ID_STATUS_PUT = "传送落子位置";public static final String ID_STATUS_GET = "获取落子位置";public static final String ID_STATUS_OVER = "对局结束";public static final String ID_STATUS_MSG = "聊天消息";public static final String ID_STATUS_BACK = "请求悔棋";public static final String ID_STATUS_FAIL = "认输";public static final String ID_STATUS_HANDSNAKE = "初次握手";public static final String ID_STATUS_BACK_RESULT = "请求悔棋结果";public static final String ID_STATUS_OVERTIME = "游戏超时";public static final String SIZE = "棋盘长度";public static final String EXISTS = "该用户名已存在系统中";public static final String USER_NAME = "userName";public static final String INIT_SUCCESS = "初始化成功";public static final String X = "x";public static final String Y = "y";public static final String STATUS = "status"; // 当前棋子的状态public static final String COLOR = "落子颜色";public static final String SYSTEM_MSG = "系统消息";// key - valuepublic static final String MY = "my"; // 玩家public static final String YOU = "you"; // 对家public static final String FIRST = "先手方"; // 1:先手; 0:后手// 属于页面的专属数据private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");public static String getId() {return getSysDate() + "-" + UUID.randomUUID().toString();}public static String getSysDate(){return sdf.format(new Date());}//  public static void main(String[] args) {
//      System.out.println(Const.getId());
//  }
}

2:服务端构建

客户端的连接信息将会在服务器定义一个Map保存,该实体类定义如下:
保存客户端所有信息的Map:private static volatile Map<String, UserData> client = new HashMap<>();
实体类定义(类名:UserData):private MessageFormat msgFormat; // 处理消息的对象private Socket socket; // 客户端套接字对象private Thread thread; // 处理消息的线程private String userId; // 登陆的用户(唯一性)private String userName; // 客户的名字(自己取的名称)private String status; // 状态(0:登陆; 1:准备; 2:对局开始; 3:观战)private String enemy; // 对家private String isFirst; // 是否为先手private List<Chess> chessBoard; // 棋盘private boolean isOver; // 对局是否结束
定义一个普通的启动类,继承Thread,重写Thread类中的run方法以监听客户端socket请求,其中MessageFormat类是处理客户端的发送/传输类,每个客户端连接后都会生成一个inputstream与outputstream(I/O)流,MessageFormat对其进行了相关的封装,也是统一处理客户端发送的信息和发送信息给客户端,在下面我会重点介绍该类,该类也是处理消息的一个核心类。
private ServerSocket ss;
private Socket s;
private boolean start = false;
private static volatile Map<String, UserData> client = new HashMap<>();public static void main(String[] args) {SuperServlet servlet = new SuperServlet();servlet.startService();
}public SuperServlet() {try {ss = new ServerSocket(Const.PORT);} catch (IOException e) {e.printStackTrace();}
}// 线程用来接收客户端连接的请求
@Override
public void run() {try {while (start) {s = ss.accept(); // 一直监听客户端的请求synchronized (s) {String userId = Const.getId(); // 返回一个唯一idMessageFormat cs = new MessageFormat(s); // 建立传输入数据的线程(服务类)// 为每个连接的客户端定义一个线程为其服务Thread th = new Thread(cs); // 定义用户信息类UserData ud = new UserData();ud.setUserId(userId); ud.setMsgFormat(cs);ud.setSocket(s);ud.setThread(th);client.put(userId, ud); // 将这个用户保存到服务器中th.start(); // 启动这个线程// 将识别ID发给客户端保存JSONObject responseMsg = new JSONObject();responseMsg.put(Const.ID, Const.ID_STATUS_HANDSNAKE);responseMsg.put(Const.MSG, userId);cs.send(responseMsg); // 发送消息给客户端System.out.println("一个客户连接... 在线数量:" + client.size());}}} catch (Exception er) {System.out.println("服务已启动或端口被占用!");er.printStackTrace();System.exit(0);}
}

2.1:服务器之MessageFormat类

前面我讲到,该类处理客户端的请求与服务端发送给该客户端的请求,为不给服务器造成阻塞,该类肯定是一个线程,简单的看一下该类定义的几个方法:
// 默认构造器,对新连接的客户端的I/O流进行封装
public MessageFormat(Socket s)
// 重写该线程的run方法,用于循环监听客户端的发送的信息
public void run()
// 发送信息给该客户端
public void send(final JSONObject msg)
// 该客户端主动发送信息给别的客户端
public void send(String userId, JSONObject msg)
// 客户端断开时关闭I/O流
public void close()
// 消息处理中心(msg为客户端发过来的json数据)
public void addInfo(String msg)
服务端重点处理的是客户端发过来的信息,所以先来讲解处理的方式:

对着下面的代码,我们先是循环监听客户端发过来的信息,dis.readUTF()是一个阻塞式的方法,若客户端发过来信息则返回一个字符串,接着在这里只是进行简单的打印就将信息传送给了addInfo() 这个方法;在我的catch中进行了相关处理,这里主要处理客户端若遇到不可描述的事情断开后,服务器应主动踢出这个客户端,这里我是先遍历了服务器中所有的客户端,找到一个线程ID与之匹配的,然后把这个线程踢出Map

@Override
public void run() {while (clientStart) {try {// 客户端传来的信息String requestMsg = dis.readUTF(); System.out.println("service do  in:" + requestMsg);// LogUtils.write("service do  in:" + requestMsg);// 消息处理addInfo(requestMsg);} catch (IOException e) {// 如果无法接收客户端的信息for(Entry<String, UserData> set : SuperServlet.getClient().entrySet()){String userId = set.getKey(); UserData ud = set.getValue();// 匹配退出的客户端进程if(this == ud.getMsgFormat()){// 给对手发送退出信息JSONObject responseMsg = new JSONObject();responseMsg.put(Const.ID, Const.ID_STATUS_FAIL);UserData clientData = SuperServlet.getClient().get(ud.getEnemy());if(clientData != null && !clientData.isOver()){clientData.getMsgFormat().send(responseMsg);}MessageFormatHelper.ppList.remove(ud.getUserId());System.out.println("退出的客户端为:"+ ud.getUserName() + " --> " + ud.getUserId());SuperServlet.getClient().remove(userId); // 服务器移除这个用户System.out.println("在线数量:" + SuperServlet.getClient().size());// ud.getThread().interrupt(); // 等待线程关闭close(); // 关闭相应的流clientStart = false; // 预先停止 while循环ud.getThread().stop(); // 等待线程关闭}}}}
}
由上面的代码可以看到,客户端的所有信息我是将由addInfo()方法处理的:

对着下面代码,我们先是将传过来的信息进行转义替换,然后转换成JSON,取到开始时常量类里面定义的key,进行switch匹配,匹配到对应的ID就做对应的事,这种做法在SpringMVC中跟RequestMapper有着异曲同工之想法。在这里,有一处地方我没有做讲解,MessageFormatHelper这个类是做什么的呢?在我们写Javaweb项目时,常会写一个dao 与 daoImpl,在这里MessageFormatHelper相当于 daoImpl类,也就是实现具体功能的类。

public void addInfo(String msg) {JSONObject json = JSONObject.fromObject(msg.replaceAll("\"", "\\\""));MessageFormatHelper.initialized(json); // 优先初始化服务器数据String id = json.get(Const.ID).toString();switch (id) {// 初始化case Const.ID_STATUS_INIT:MessageFormatHelper.init();break;// 匹配玩家case Const.ID_STATUS_PP:MessageFormatHelper.matchUser();break;// 落子case Const.ID_STATUS_PUT:MessageFormatHelper.putChess();break;// 获取棋子
//      case Const.ID_STATUS_GET:
//          MessageFormatHelper.getChess();
//          break;// 对局结束case Const.ID_STATUS_OVER:MessageFormatHelper.gameOver();break;// 悔棋请求case Const.ID_STATUS_BACK:MessageFormatHelper.toBack();break;// 悔棋请求的结果case Const.ID_STATUS_BACK_RESULT:MessageFormatHelper.toBackResult();break;// 认输case Const.ID_STATUS_FAIL:MessageFormatHelper.fail();break;// 聊天消息case Const.ID_STATUS_MSG:MessageFormatHelper.chatMsg();break;// 游戏超时case Const.ID_STATUS_OVERTIME:MessageFormatHelper.overTime();break;default:System.out.println("server: 未匹配到分支; id:" + id);break;}// 未找到匹配分支
//      JSONObject result = new JSONObject();
//      result.put(Const.ID, "未找到匹配分支");
//      return result;
}
MessageFormatHelper类实现的功能在MessageFormat中皆有体现并有注释,因代码太多,大家自行下载代码研究,我的代码注释一般都写的比较详细,且思路通俗易懂;这里贴几个少一点的功能代码
/*** 双方棋子互传*/
public static void putChess() {// 设置我方棋盘String userId = json.getString(Const.MY);// 获取坐标int x = json.getInt(Const.X);int y = json.getInt(Const.Y);String color = json.getString(Const.COLOR);// 更新我方棋盘UserData my = SuperServlet.getClient().get(userId);my.getChessBoard().add(new Chess(x, y, color));// 更新对方棋盘UserData you = SuperServlet.getClient().get(my.getEnemy());you.getChessBoard().add(new Chess(x, y, color));// 更新服务器数据SuperServlet.updateClient(userId, my);SuperServlet.updateClient(you.getUserId(), you);// 将棋子同步给对手if(!you.isOver()){json.put(Const.ID, Const.ID_STATUS_GET);you.getMsgFormat().send(json);} else {System.out.println("无将棋子同步给对手, 对方已结束游戏");}
}/*** 聊天消息处理*/
public static void chatMsg() {result.put(Const.ID, Const.ID_STATUS_MSG);String userId = json.getString(Const.MY);UserData my = SuperServlet.getClient().get(userId);// 获取对手名称UserData you = SuperServlet.getClient().get(my.getEnemy());if(you != null){result.put(Const.MSG, json.getString(Const.MSG));result.put(Const.MY, my.getUserName());you.getMsgFormat().send(result);}
}/*** 悔棋请求*/
public static void toBack(){String userId = json.getString(Const.MY);UserData my = SuperServlet.getClient().get(userId);result.put(Const.ID, Const.ID_STATUS_BACK);UserData you = SuperServlet.getClient().get(my.getEnemy());you.getMsgFormat().send(result);
}/*** 悔棋结果*/
public static void toBackResult(){String userId = json.getString(Const.MY);UserData my = SuperServlet.getClient().get(userId);result.put(Const.ID, Const.ID_STATUS_BACK_RESULT);result.put(Const.MSG, json.getString(Const.MSG));UserData you = SuperServlet.getClient().get(my.getEnemy());you.getMsgFormat().send(result);// 更新服务器棋盘以备观战if("同意".equals(json.getString(Const.MSG))){my.getChessBoard().remove(my.getChessBoard().size() - 1);you.getChessBoard().remove(you.getChessBoard().size() - 1);SuperServlet.updateClient(my.getUserId(), my);SuperServlet.updateClient(you.getUserId(), you);}
}

3:客户端

客户端传送信息与服务器交互采用的方式跟服务器处理信息的方式是一样的,所以没有什么可讲的,主要讲一下棋盘的绘制与如何判断已经胜利。

3.1:客户端之JPanel

在这个之前还有一个JFrame,学过awt的应该知道 frame,而JFrame则是swing,swing是AWT的之类,相关资料可以查询API,JPanel是一个容器,而JFrame是一个窗口,这里直接跳过JFrame讲JPanel,因为绘制棋盘的甩有功能都是在这个容器里面实现的
/*** 相关属性定义*/
private int span = 35; // 棋盘格子宽度
private int margin = 22;
private final int DIAMETER = 35; // 直径
private final int row = 15; // 棋盘行、列
private final int col = 15;
private int i = 0;
private boolean isBlack = true;
private boolean isPicture = true;// 是否用图片作为背景(图片是正常游戏背景,false为测试游戏背景)
private ImageIcon img = new ImageIcon("src/images/board.jpg");
private List<Chess> list = new LinkedList<Chess>(); // 整个棋盘
public boolean gameOver = true; // 默认结束游戏// 网络数据
public boolean isNetworkPK = false;
public boolean myChessColor = false; // 记录我方落子的颜色
public boolean failFlag = false; // 认输标记
public MessageQueuePanel MQPanel;
public String userName = null; // 我的名称
public String userId = null; // 我的ID

以上定义中,棋盘就是我们的List,重写 JPanel 的Paint方法绘制游戏棋盘

@Override
public void paint(Graphics g) {super.paint(g);// span = this.getHeight() / row; // 当窗口被拖动,动态刷新窗口Graphics2D g2 = (Graphics2D) g;// 正常游戏绘制的棋盘是一张背景图片g.drawImage(img.getImage(), 1, 0, null); RadialGradientPaint rgp = null;// 画棋子for (i = 0; i < list.size(); ++i) {Chess chess = list.get(i);int xPos = chess.getX() * span + margin; // 将真实坐标转换成网格坐标int yPos = chess.getY() * span + margin;g2.setColor(chess.getColors()); // 设置画笔颜色if (chess.getColors() == Color.BLACK) {rgp = new RadialGradientPaint(xPos - DIAMETER / 2 + 26, //yPos - DIAMETER / 2 + 12, 20, new float[] { 0.0f, 1.0f }, //new Color[] { Color.WHITE, Color.BLACK });g2.setPaint(rgp);} else {// x, y, 直径, 渐变度, 渐变色rgp = new RadialGradientPaint(xPos - DIAMETER / 2 + 25, //yPos - DIAMETER / 2 - 30, 60, new float[] { 0f, 1f }, //new Color[] { Color.BLACK, Color.WHITE });g2.setPaint(rgp);}// 去锯齿g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_DEFAULT);g2.fillOval(xPos - DIAMETER / 2, yPos - DIAMETER / 2, span - 1, span);// 画红色矩形g2.setColor(Color.RED);if (i == list.size() - 1)g2.draw3DRect(xPos - DIAMETER / 2 - 1, // yPos - DIAMETER / 2 - 1, span, span + 1, true);}
}

3.2:客户端之如何判断胜利

在isWin方法中,我们传入整个棋盘,以及当前的落子位置和当前落子的颜色,五子棋判断胜利相关于一个‘米’字,实则只需要扫描一条边后,然后判断一下同颜色的连子数是否大于等于五个就可判断它是否已经胜利了,在代码中前两个循环是判断第一条横线,分别判断从落子位置的左边和右边,也就是米字中的 一 ,其它判断通俗易懂,大家可自行领悟
// 判断胜利的方法
private boolean isWin(List<Chess> list, int xPos, int yPos, boolean isBlack) {int chessCount = 1;final int max = 5; // 连子数int x = 0, y = 0;Color color = (isBlack ? Color.BLACK : Color.WHITE);// 当前位置向左for (x = xPos - 1; x >= 0; --x) {if (getChess(list, x, yPos, color) != null)chessCount++;elsebreak;}// 当前位置向右for (x = xPos + 1; x <= row; ++x) {if (getChess(list, x, yPos, color) != null)chessCount++;elsebreak;}if (chessCount >= max)return true;elsechessCount = 1;// 当前位置向上for (y = yPos - 1; y >= 0; --y) {if (getChess(list, xPos, y, color) != null)chessCount++;elsebreak;}// 当前位置向下for (y = yPos + 1; y <= col; ++y) {if (getChess(list, xPos, y, color) != null)chessCount++;elsebreak;}if (chessCount >= max)return true;elsechessCount = 1;// 左斜着向上for (x = xPos - 1, y = yPos - 1; x >= 0 && y >= 0; --x, --y) {if (getChess(list, x, y, color) != null)chessCount++;elsebreak;}// 右斜着向下for (x = xPos + 1, y = yPos + 1; x <= row && y <= col; ++x, ++y) {if (getChess(list, x, y, color) != null)chessCount++;elsebreak;}if (chessCount >= max)return true;elsechessCount = 1;// 右斜着向上for (x = xPos + 1, y = yPos - 1; x <= row && y >= 0; ++x, --y) {if (getChess(list, x, y, color) != null)chessCount++;elsebreak;}// 左斜着向下for (x = xPos - 1, y = yPos + 1; x >= 0 && y <= col; --x, ++y) {if (getChess(list, x, y, color) != null)chessCount++;elsebreak;}if (chessCount >= max)return true;elsechessCount = 1;return false;
}
完整服务器代码与客户端代码链接:https://download.csdn.net/download/qq_20698983/10448331

Java swing + socket 写的一个五子棋网络对战游戏相关推荐

  1. 五子棋网络对战 java实现

    五子棋网络对战 java实现 最近期末考试,压力实在是太大了.专业不对口的痛苦,别人永远体会不来.闲暇之余,便想着巩固一下java的基础知识,以前看别人写的游戏,心里都觉得哇 好厉害啊,我什么时候才可 ...

  2. 基于Java+Swing+Socket实现泡泡堂游戏

    基于Java+Swing+Socket实现泡泡堂游戏 一.功能展示 1.游戏登陆 2.房间 3.对战 二.代码展示 三.其他系统 前言 <泡泡堂>是由韩国游戏公司Nexon开发的一款休闲游 ...

  3. java五子棋联网对战,毕业设计-- 用JAVA实现五子棋网络对战系统

    毕业设计-- 用JAVA实现五子棋网络对战系统 本科生毕业设计(论文)本科生毕业设计(论文) 题 目 用 JAVA 实现五子棋网络对战系统 姓 名 xxxx 学 号 xxxxx 系 别 计算机工程系 ...

  4. 用js写出数据结构中的自定义队列,利用队列思想写出一个击鼓传花的游戏函数,优先级队列

    队列的核心是先进先出 1.用js写出数据结构中的自定义队列 class Queue{constructor(){this.item = [];}// 1.入队enqueue(ele){this.ite ...

  5. 我用Python写了一个成语接龙的游戏,并把它部署到了手机上

    我用Python写了一个成语接龙的游戏,并把它部署到了手机上 成语大全数据 6月高考的前一天,我发布的一篇文章,决战高考,帮你秒变成语之王,当时只是把 http://chengyu.haoshiwen ...

  6. python做一个简单的对战游戏

    今天没什么事情,就自己做了一个简单的对战游戏. 这个小游戏流程大概是这样的 开始游戏 ↓ 选择人物出场顺序及技能 ↓  ←  ←  ←  ←  ←  ←  ←  ←  ← ← ↖ 开始战斗→胜利方→+ ...

  7. 如果用java swing编写一个五子棋(人人对战)

    2020博客之星年度总评选进行中:请为74号的狗子投上宝贵的一票! 我的投票地址:点击为我投票 写在前面: 大家好,我是 花狗Fdog ,来自内蒙古的一个小城市,目前在泰州读书. 很感谢能有这样一个平 ...

  8. 用java的socket来发送一个类

    用socket可以简单的发送一些文本信息,太复杂的可能发送不了,比如图片音频可能要用到http来发送和接收了.最基本的使用socket来发送一个字符串,但有的时候我们希望能够发送一个类,这样serve ...

  9. 基于Java swing 和多线程开发一个猜拳游戏:有甲乙双方猜拳,投注其中一方,猜赢家,猜中即赢,猜错即输.

    1,系统简要说明:系统利用Java Swing创建窗口以及按钮,内置多线程实现玩家进行猜测之后,甲乙随机图片的转动,按停止按钮后停止图片的转动,并用Text文本框输出结果. 利用多线程以及生成随机数进 ...

最新文章

  1. 红绿灯问题(位运算)
  2. AUC、Precision、Recall、F-measure、Accuracy
  3. 共模电感适用的频率_分析共模电感和差模电感寄生电容抵消的方法
  4. jQuery.extend
  5. 解决ASP.NET Core部署到IIS,更新项目另一个程序正在使用此文件,进程无法访问...
  6. linux 系统安装mongodb数据库---方法2
  7. mysql获取离当前数据最近的数据_Mysql 获取最近数据信息
  8. bootstrap外不引用连接_网络编程Netty IoT百万长连接优化,万字长文精讲
  9. 设计模式(17) 访问者模式(VISITOR) C++实现
  10. python入门难?几个练习实例 全会就入门了
  11. node.js express架构安装部署
  12. 如何提高服务器网站网速,怎么提升服务器网速
  13. AbstractQueuedSynchronizer源码分析
  14. iso硬盘安装 凤凰os_Phoenix OS硬盘版|凤凰系统x86硬盘版下载 v2.5.0正式版 附安装教程 - 121下载站...
  15. linux系统安装花生壳
  16. 阿里云DataV实现MySQL数据库类型数据源绑定
  17. toolbar遇到问题和解决方式
  18. 微软 Win10 更新再出 bug
  19. 学习管理系统五大好处
  20. 最全大数据就业前景分析!此篇文章给你答案

热门文章

  1. java简单的正则表达式验证邮箱
  2. Ubuntu22.04 安装禅道
  3. 禅道项目管理,带你避开项目管理中的那些坑
  4. 计算机网络:自顶向下方法 笔记
  5. 使用chrome设置代理,果然还是linux 方便呢。想安装一个chrome的插件,通过命令设置代理。
  6. 微信域名防封PHP程序强制跳转到浏览器打开
  7. 中文自然语言处理语言资源项目(ChineseNLPcorpus)
  8. 16光8电全千兆宽温工业交换机16千兆光8千兆网口机架式网管型工业级以太网交换机
  9. BELLMAN-FORD算法 求有边数限制的最短路
  10. OpenHarmony OpenSl ES音频播放开发