当前位置: 移动技术网 > 网络运营>网络>协议 > 荐 基于UDP协议的中国象棋游戏实现!干货满满!

荐 基于UDP协议的中国象棋游戏实现!干货满满!

2020年07月15日  | 移动技术网网络运营  | 我要评论

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;

/**
 * 背景图片面板
 * @author TT
 * @create 2020-06-10-1:17
 */
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;

/**
 * 客户端信息输入界面
 * @author TT
 * @create 2020-06-10-8:09
 */
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;

/**
 * 开始界面
 * @author TT
 * @create 2020-06-10-1:11
 */
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;

/**
 * @author D、
 * @create 2020-06-02-17:01
 */
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;

/**
 * 音效工具类
 * @author D、
 * @create 2020-06-02-17:32
 */
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.*;

/**
 * 类似于C语言的结构体
 * 棋子所属玩家,棋子类别,棋子所在行数、棋子所在列数、棋子图案
 * @author TT
 * @create 2020-06-10-20:06
 */
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)类:

/**
 * 游戏规则
 * @author TT
 * @create 2020-06-11-22:12
 */
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类:

/**
 * 存储棋谱信息,实现"悔棋"功能
 * @author TT
 * @create 2020-06-11-8:17
 */
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( ̄▽ ̄)ブ,有问题可在文章底部进行评论探讨哦!

本文地址:https://blog.csdn.net/ttbro/article/details/107308514

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网