基于UDP协议的中国象棋游戏
- 1、效果图
- 2、项目阐述
- 3、项目知识点
- 4、部分界面实现
- 4.1、背景界面面板
- 4.2、输入客户端信息界面面板
- 4.3、主界面
- 5、功能实现
- 5.1、界面切换
- 5.2、音效实现
- 5.3、联网功能实现(UDP协议)
- 5.4、期盘功能实现
- 5.4.1、棋盘以及棋子的绘画
- 5.4.2、下棋
- 5.4.3、悔棋
- 5.4.4、认输
- 6、总结
1、效果图
2、项目阐述
ps:由于代码量较多,就不放上完整代码及素材等资源啦~若有需要可到主页下载哦!
以下代码均为主要实现代码o( ̄▽ ̄)ブ
- 本项目基于UDP协议,实现一个GUI界面的象棋游戏。要求实现玩家对战玩家、悔棋、认输、退出等功能,以及实现多个界面,如初始界面、游戏规则界面、开发团队界面、输入信息界面、游戏界面等界面之间的切换,并且实现点击时触发音效。
- 开发软件:IDEA
3、项目知识点
- UDP协议:用于实现玩家与玩家之间的联网操作协议。
- 多线程:实现玩家与玩家之间的 " 即时通讯 "。
- I/O流:实现点击音效
- GUI:实现界面
4、部分界面实现
4.1、背景界面面板
项目中的大部分界面面板都是继承于“背景界面”类,实现背景渲染的功能。
package ChineseChess;
import javax.swing.*;
import java.awt.*;
import java.net.URL;
public class JPanel_Background extends JPanel{
//标识符
private static final long serialVersionUID = 1L;
//图片对象
Image image;
//构造器
public JPanel_Background() {
setOpaque(false);//设置透明色!必须设置,否则显示不出背景!
URL url = JFrame_Start.class.getResource("../static/StartBackground.jpg");
image=new ImageIcon(url).getImage();
}
//重写paint函数,将背景图片绘画上去
@Override
public void paint(Graphics g) {
//具体参数信息
//drawImage(图片对象,起始点X坐标,起始点Y坐标,宽度,高度,绘画在哪个面板上)
g.drawImage(image,0,0,image.getWidth(this),image.getHeight(this),this);
super.paint(g);
}
}
4.2、输入客户端信息界面面板
获取IP地址及端口号,组件包括JLabel、JTextField、JButton
package ChineseChess;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class JPanel_input extends JPanel_Background{
//标识符
private static final long serialVersionUID = 1L;
JLabel label_title;//输入客户端信息
JLabel label_ip;//IP
JLabel label_port;//端口号
JTextField field_IP;//输入IP
JTextField field_port;//输入IP
public JPanel_input(){
System.out.println("输入界面已运行");
//绝对布局
setLayout(null);
//组件添加
label_title = new JLabel("输入客户端信息");
label_title.setBounds(290,0,692,200);
label_title.setFont(new Font("",1,40));
add(label_title);
label_ip = new JLabel("请输入IP:");
label_ip.setBounds(175,300,200,100);
label_ip.setFont(new Font("",1,20));
add(label_ip);
field_IP = new JTextField();
field_IP.setBounds(280,340,300,30);
add(field_IP);
label_port = new JLabel("请输入对方端口号:");
label_port.setBounds(85,400,200,100);
label_port.setFont(new Font("",1,20));
add(label_port);
field_port = new JTextField();
field_port.setBounds(280,440,300,30);
add(field_port);
}
}
4.3、主界面
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class JFrame_Start extends JFrame implements ActionListener {
//标识符
private static final long serialVersionUID = 1244L;
JPanel_Background panel;//背景面板
JButton button_back_help;//"帮助"面板的返回按钮
JButton button_start;//开始按钮
JButton button_help;//帮助按钮
JButton button_exit;//退出按钮
JButton button_concern;//确认按钮
JButton button_back_team;//"开发团队"面板的返回按钮
JButton button_team;//开发团队按钮
JLabel label1;//中国象棋
//构造器
public JFrame_Start(){
//调用自定义类(如"JPanel_Background")创建面板对象
panel = new JPanel_Background();
Data.panel_input = new JPanel_input();
Data.panel_help = new JPanel_Help();
Data.panel_teamMender = new JPanel_TeamMender();
//将面板布局设置为"绝对布局"
panel.setLayout(null);
//创建组件对象
button_start = new JButton("开始");
button_help = new JButton("帮助");
button_exit = new JButton("退出");
button_team = new JButton("开发团队");
label1 = new JLabel("中国象棋");
//将组件添加到面板上
label1.setBounds(320,0,692,200);
label1.setFont(new Font("",1,60));
panel.add(label1);
button_start.setBounds(380,250,100,50);
panel.add(button_start);
button_help.setBounds(380,350,100,50);
panel.add(button_help);
button_team.setBounds(380,450,100,50);
panel.add(button_team);
button_exit.setBounds(380,550,100,50);
panel.add(button_exit);
//将面板添加到容器上
Container container = getContentPane();
container.add(panel);
//窗口相关属性设置
setTitle("中国象棋");
setResizable(false);
setVisible(true);
setSize(880,820);
this.addWindowListener(new WindowAdapter() {//绑定关闭窗口事件
@Override
public void windowClosing(WindowEvent e) {
if (!Data.isStart){
System.exit(0);
}else {
Data.panel_game.send("quit|");//若一方退出游戏窗口,则游戏结束
System.exit(0);
}
}
});
//为主面板的button组件添加监听事件
button_start.addActionListener(this);
button_help.addActionListener(this);
button_exit.addActionListener(this);
button_team.addActionListener(this);
//为“帮助”面板添加按钮
button_back_help = new JButton("返回");
button_back_help.setBounds(700,650,100,50);
Data.panel_help.add(button_back_help);
button_back_help.addActionListener(this);
//为“客户端信息输入”面板添加按钮
button_concern = new JButton("确定");
button_concern.setBounds(700,650,100,50);
Data.panel_input.add(button_concern);
button_concern.addActionListener(this);
//为“开发团队”面板添加按钮
button_back_team = new JButton("返回");
button_back_team.setBounds(700,650,100,50);
Data.panel_teamMender.add(button_back_team);
button_back_team.addActionListener(this);
}
//监听事件请看“功能实现”部分
......
5、功能实现
5.1、界面切换
- 思想:“一个窗口,多个面板”
- 实现方法:将各个面板类对象创建为全局常量,在窗口类绑定“切换面板”事件
......
//按钮监听事件
@Override
public void actionPerformed(ActionEvent e) {
Object source = e.getSource();
if (button_start.equals(source)){//开始按钮,进入客户端信息输入界面
Sound.click();//添加音效
changeContentPanel(Data.panel_input);//切换界面
}
//其他面板切换
......
}
//界面切换
public void changeContentPanel(Container contentPanel){
this.setContentPane(contentPanel);//就所需的面板放置进去
this.revalidate();//重新计算组件的大小,并自动布局
}
5.2、音效实现
- 利用I/O流将音效文件导入
- 以启动线程的方式实现音效效果
MusicPlayer类:
package ChineseChess;
import javax.sound.sampled.*;
import java.io.File;
import java.io.FileNotFoundException;
public class MusicPlayer implements Runnable{
File soundFile;//音乐文件
Thread thread;//父线程
boolean circulate;//是否循环播放
public MusicPlayer(String filepath, boolean circulate) throws FileNotFoundException {
this.circulate = circulate;
soundFile = new File(filepath);
if(!soundFile.exists()){
throw new FileNotFoundException(filepath + "未找到");
}
}
public void play(){
thread = new Thread(this);//创建线程对象
thread.start();
}
public void stop(){
thread.stop();//强制关闭线程
}
@Override
public void run() {
byte[] auBuffer = new byte[1024 * 128];//创建128k的缓冲区
do{
AudioInputStream audioInputStream = null;
SourceDataLine auline = null;
try {
//从音乐文件中获取音频输入流
audioInputStream = AudioSystem.getAudioInputStream(soundFile);
AudioFormat format = audioInputStream.getFormat();//获取音频格式
//按照源数据行类型和指定音频格式创建数据行对象
DataLine.Info info = new DataLine.Info(SourceDataLine.class,format);
//利用音频系统类获得与指定Line.Info 对象中的描述匹配的行,并转换为源数据行对象
auline = (SourceDataLine) AudioSystem.getLine(info);
auline.open(format);//按照指定格式打开源数据行
auline.start();//源数据开启读写活动
int byteCount = 0;//记录音频输入流读出的字节数
while (byteCount != -1){
byteCount = audioInputStream.read(auBuffer,0,auBuffer.length);
if (byteCount >= 0){
auline.write(auBuffer,0,byteCount);//这里涉及到了IO流的知识
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
auline.drain();//清空数据行
auline.close();//关闭数据行
}
}while (circulate);//判断是否循环播放
}
}
Sound类:
package ChineseChess;
import java.io.FileNotFoundException;
public class Sound {
//播放声音
private static void play(String file,boolean circulate){
MusicPlayer player = null;//创建播放器
try {
player = new MusicPlayer(file, circulate);
player.play();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
public static void hit(){
play("src/music/hit.wav",false);//获取音乐文件
}
public static void click(){
play("src/music/click.wav",false);
}
public static void refuse(){
play("src/music/refuse.wav",false);
}
}
5.3、联网功能实现(UDP协议)
ps:由于涉及代码较多,就说一下一些关键点(完整代码可到主页下载资源哦)
- 利用“多线程”的知识,每次执行一个操作便给另一个端口(即对方)发送消息,如请求悔棋,则可发送“ask”的消息到另一个端口
- 对方端口则对接收到的消息进行判断,并通过具体的功能方法响应接收到的消息
- 若对于线程的知识不太了解,推荐参考主页“聊天室”文章哦~
5.4、期盘功能实现
5.4.1、棋盘以及棋子的绘画
- 绘制棋盘,其实就是采用“添加背景”的方式,找到棋盘图,导入并利用绘画函数,进行绘画,即可实现棋盘效果。同理,棋子也是利用该方法进行绘画,确定好每一个棋子的大小,绘画即可。
(下面附上paint方法代码)
//重写paint函数,将背景图片绘画上去
@Override
public void paint(Graphics g) {
//判断是否轮到本方下棋,以便判定是否切换背景
if (!Data.isStart){
URL url = JFrame_Start.class.getResource("../static/chessBoard_Test3.jpg");//灰色背景,即未联网状态
image=new ImageIcon(url).getImage();
}else if (Data.isPlayer){
URL url = JFrame_Start.class.getResource("../static/chessBoard_Test1.jpg");//绿色背景
image=new ImageIcon(url).getImage();
}else{
URL url = JFrame_Start.class.getResource("../static/chessBoard_Test2.jpg");//红色背景
image=new ImageIcon(url).getImage();
}
//drawImage(图片对象,起始点X坐标,起始点Y坐标,宽度,高度,绘画在哪个面板上)
g.drawImage(image,0,0,image.getWidth(this),image.getHeight(this),this);
super.paint(g);
for (int i = 0; i < 32; i++) {
if (Data.chess[i] != null){
Data.chess[i].paint(g,this);
}
}
//绘画选中框
if (firstChess != null && firstChess.player == Data.localPlayer){
//白色边框
g.setColor(Color.WHITE);
firstChess.drawSelectedChess(g);
g.setColor(Color.BLACK);
//只要改变了画笔的颜色,必须改变回来,方便后面使用
}
if (secondChess != null){
g.setColor(Color.WHITE);
secondChess.drawSelectedChess(g);
g.setColor(Color.BLACK);
}
}
ps:主要实现如上所示,由于代码稍微复杂,可能一些地方看起来不容易理解,不过只需掌握主要实现思想即可~
5.4.2、下棋
- 下棋,实际上是对每一次操作进行重新绘画,从而实现下棋效果。
- 棋子类存储相应的属性以及方法,由于象棋有32个棋子,因此创建一个容量为32的Chess型数组。
- 棋盘采用的是一个二维数组,即10行9列的一个10*9的数组,对应棋盘上的每一个交点。
- 当交点处没有棋子时,则其值为-1。(即数组初始化操作)
- 当交点处由棋子时,则其值为对应的棋子数组的下标。
- 每一次操作,即更改棋盘上的点对应的值,并且进行重画,从而实现下棋。
Chess类:
import javax.swing.*;
import java.awt.*;
public class Chess {
public int player;//棋子所属玩家
public String type;//棋子类别
public int x;//棋子所在列
public int y;//棋子所在行
public Image chessImage;//棋子图案
//空参构造器
public Chess(){}
//带参构造器
public Chess(int player,String type,int x,int y){
this.player = player;
this.type = type;
this.x = x;
this.y = y;
if (player == Data.RedPlayer){
switch (type){
case "帅":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess7RedURL);
break;
case "仕":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess8RedURL);
break;
case "相":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess9RedURL);
break;
case "马":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess10RedURL);
break;
case "车":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess11RedURL);
break;
case "炮":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess12RedURL);
break;
case "兵":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess13RedURL);
break;
}
}else{
switch (type){
case "将":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess0BlackURL);
break;
case "士":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess1BlackURL);
break;
case "象":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess2BlackURL);
break;
case "马":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess3BlackURL);
break;
case "车":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess4BlackURL);
break;
case "炮":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess5BlackURL);
break;
case "卒":
chessImage = Toolkit.getDefaultToolkit().getImage(Data.chess6BlackURL);
break;
}
}
}
//下棋
public void setPos(int x,int y){
this.x = x;
this.y = y;
}
//翻转棋子,实现不同端口显示下棋效果
public void ReversePos(){
x = 9 - x;
y = 8 - y;
}
//绘画棋子
public void paint(Graphics g , JPanel i){
g.drawImage(chessImage,Data.startX + y*72,Data.startY + x*74,68,68,i);
}
//绘画选中框
public void drawSelectedChess(Graphics g){
g.drawRect(Data.startX + y*72,Data.startY + x*74,68,68);
}
}
游戏规则(Rule)类:
public class Rule {
Chess chess;//移动的棋子
int oldX,oldY;//原先的坐标
int newX,newY;//新的坐标
String chessName;//棋子的种类
public Rule(Chess chess, int newX, int newY){
this.chess = chess;
this.newX = newX;
this.newY = newY;
this.chessName = chess.type;
}
public boolean IsAbleToMove(){
oldX = chess.x;
oldY = chess.y;
//"将" Or "帅"
if ("将".equals(chessName) || "帅".equals(chessName)){
//"将"吃"帅",判断二者是否同列,且保证二者之间不存在任何棋子
if (oldY == newY && (Data.chessBoard[newX][newY]==0 || Data.chessBoard[newX][newY]==16)){
for (int i = newX-1; i >oldX ; i--) {
if (Data.chessBoard[i][newY] != -1){
return false;
}
}
return true;
}
//斜着走
if ((newX-oldX) * (newY-oldY) != 0){
return false;
}
//下棋距离超过一格
if (Math.abs(newX-oldX)>1 ||Math.abs(newY-oldY)>1){
return false;
}
//超出九宫格区域
if ((newX>2 && newX<7) || newY>5 || newY<3){
return false;
}
return true;
}
//"士" Or "仕"
if ("士".equals(chessName) || "仕".equals(chessName)){
//横着走或者竖着走
if ((newX - oldX) * (newY - oldY) == 0){
return false;
}
//如果斜走距离超过一格,即判断横向或者纵向的位移量是否大于一
if (Math.abs(newX-oldX)>1 ||Math.abs(newY-oldY)>1){
return false;
}
//超出九宫格区域
if ((newX>2 && newX<7) || newY>5 || newY<3){
return false;
}
return true;
}
//"象" Or "相"
if ("相".equals(chessName) || "象".equals(chessName)){
//横着走或者竖着走
if ((newX - oldX) * (newY - oldY) == 0){
return false;
}
//如果不是走"田"字形,即横向或者纵向距离不同时为2
if (Math.abs(newX-oldX)!=2 ||Math.abs(newY-oldY)!=2){
return false;
}
//如果象越"楚河-汉界"
if (newX<5){
return false;
}
int i=0,j=0;//记录象眼位置
if (newX - oldX == 2){ //象向下跳
i = oldX+1;
}
if (newX - oldX == -2){ //象向上跳
i = oldX -1;
}
if (newY - oldY == 2){ //象向右跳
j = oldY+1;
}
if (newY - oldY == -2){ //象向左跳
j = oldY-1;
}
if (Data.chessBoard[i][j] != -1){ //象眼被堵
return false;
}
return true;
}
//省略其他约束
......
}
5.4.3、悔棋
- 悔棋功能有两个要求:请求悔棋、还原上一步操作
- 请求悔棋:利用线程,发送悔棋请求到对方端口,再接收来自对方端口是否同意的信息
- 还原:利用ArrayList集合,添加自定义Node泛型,记录棋谱信息。每一次悔棋,即还原集合末尾下标的存储的数据,并进行重画,即可实现悔棋。
Node类:
public class Node {
int index;//移动的棋子
int x,y;//棋子移动后的坐标
int oldX,oldY;//棋子移动前的坐标
int eatChessIndex = -1;//被吃掉的棋子,若棋子没有被吃掉,则等于-1
public Node(int index, int x, int y, int oldX, int oldY, int eatChessIndex) {
this.index = index;
this.x = x;
this.y = y;
this.oldX = oldX;
this.oldY = oldY;
this.eatChessIndex = eatChessIndex;
}
@Override
public String toString() {
return "index:" + index +" x:" + x + " y" + y + " oldX:" + oldX + " oldY:"+oldY + " eatchessindex:" + eatChessIndex;
}
}
部分“悔棋”线程代码:
......
else if (array[0].equals("agree")){//接收"agree"信息 -->同意悔棋
JOptionPane.showMessageDialog(Data.panel_game,"对方同意了你的悔棋请求");
Node temp = Data.list.get(Data.list.size()-1);//获取棋谱的最后一步棋
Data.list.remove(Data.list.size()-1);//移除最后一步信息
if (Data.localPlayer == Data.RedPlayer){//假如我是红方
if (temp.index >= 16){//上一步是我下的,退后一步
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,退后两步
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
temp = Data.list.get(Data.list.size()-1);
Data.list.remove(Data.list.size()-1);
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
}
}else {//假如我是黑方
if (temp.index < 16){//上一步是我下的,退后一步
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
}else{//上一步是对方下的,退后两步
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
temp = Data.list.get(Data.list.size()-1);
Data.list.remove(Data.list.size()-1);
backChess(temp.index,temp.x,temp.y,temp.oldX,temp.oldY);
if (temp.eatChessIndex!=-1){//上一步吃子
resetChess(temp.eatChessIndex,temp.x,temp.y);//将被吃的棋子放回棋盘
}
}
}
Data.isPlayer = true;
repaint();
}else if (array[0].equals("refuse")){//接收"refuse"信息 -->不同意悔棋
Sound.refuse();
JOptionPane.showMessageDialog(Data.panel_game,"对方拒绝了你的悔棋请求");
}
......
ps:以上部分代码可能很多看不明白,但只需理解主要思想即可,包括一些约束条件等。(完整代码可到主页下载)
5.4.4、认输
- 认输功能有两个要求:请求认输、退出游戏
- 请求认输:通过线程,发送认输请求到对方端口,再接收来自对方端口是否同意的信息
- 退出游戏:实现弹窗弹出并退出游戏
部分“认输”线程代码:
......
else if (array[0].equals("lose")){//接收"lose"信息 -->对方认输
JOptionPane.showConfirmDialog(Data.panel_game,"对方认输,比赛胜利!","对方认输",JOptionPane.DEFAULT_OPTION);
System.exit(0);
}else if (array[0].equals("quit")){//接收"quit"信息 -->对方退出游戏
JOptionPane.showConfirmDialog(Data.panel_game,"对方退出游戏,比赛结束!","对方退出",JOptionPane.DEFAULT_OPTION);
System.exit(0);
}
......
6、总结
- 个人觉得该项目的重点在于多线程部分的编写,很多小细节需要注意,耗时占比也是最大的。
- 项目改进建议:①添加重新开始游戏功能 ②添加聊天室功能。
- 由于代码量相对较多,就没有放上完整代码及素材等资源,若有需要,可到主页下载。
- 象棋实现功能参考文章:
https://blog.csdn.net/A1344714150/article/details/85724241
程序小白一枚,发表文章只是为了整理知识~也希望可以和大家一起学习进步!看完的小伙伴可以点赞收藏哦o( ̄▽ ̄)ブ,有问题可在文章底部进行评论探讨哦!