基于UDP协议的中国象棋游戏实现!

   日期:2020-07-17     浏览:106    评论:0    
核心提示:基于UDP协议的中国象棋项目1、效果图2、项目阐述3、项目知识点1、效果图2、项目阐述本项目基于UDP协议,实现一个GUI界面的象棋游戏。要求实现玩家对战玩家、悔棋、认输、退出等功能,以及实现多个界面,如初始界面、游戏规则界面、开发团队界面、输入信息界面、游戏界面等界面之间的切换,并且实现点击时触发音效。开发软件:IDEA3、项目知识点UDP协议:用于实现玩家与玩家之间的联网操作协议。多线程:实现玩家与玩家之间的 即时通讯 。I/O流:实现点击音效GUI:实现界面...

基于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( ̄▽ ̄)ブ,有问题可在文章底部进行评论探讨哦!

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服