引言
java作为后端的编程高级语言,功能齐全,表达力很强,并且目标效率高。我们可以用java来通过编写经典的五子棋游戏,来看看java中的基础知识在一个小项目当中的应用。
思路
任何一个程序项目,我们都应该从以下四个大的思路方向着手
用户交互部分
五子棋下棋的界面
所谓五子棋下棋的界面,其实就是指用户直接交互的五子棋棋盘,我们要用java来把五子棋的棋盘绘制出来
这里放一块仅仅是棋盘的图
界面上的功能按钮
可能会用到的按钮模块
开始游戏
悔棋
认输
退出游戏
用户通过点击按钮会调用的业务功能
首先请读者先自己思考一下,“用户点击按钮”这一行为,如果用java中的一个工具来很好地表达出来,那么这个工具是什么呢?
ActionListener,即动作监听器,就是一个能够完整表达点击按钮这一行为的工具。因为动作监听器,顾名思义,就是可以监听用户动作的接口,它可以控制界面上的按钮,文本框等。我们在电脑端下棋,其实就是通过用户对于界面的操作,动作监听器就会得到一个通知,开始执行我们写在这个动作监听器其中的代码指令。
用户鼠标点击进行下棋
基于上文的3,我们就能知道,当我们下棋的时候,全部的动作都来自对于鼠标的点击,那么就需要用到鼠标监听器,即MouseListener。
数据模型部分
黑白棋子的数据模型,我们要对棋子设置以下逻辑
棋子坐标
棋子落子顺序
棋子黑白标识
业务逻辑处理部分
这一部分我们主要设置以下内容
鼠标点击即可落子下棋,并且能够交替下棋
判断棋子的落子范围,准确落在棋盘落子部位
避免重复落子
实现开始游戏,判断输赢,悔棋,认输,退出游戏
数据存储部分
我们要将之前的棋子的诸多数据,能够准确完整地存储到后台数据中,主要有以下几个模块:
棋子能够显示到棋盘上
用线性表存储棋子的坐标数据
采用二维数组来存储棋盘状态(因为棋盘是一个二维的矩阵)
当棋盘状态发生变化时,重新绘制棋盘和棋子的位置
实际操作
顺着以上的思路,我们要将每一模块继续细分,而且细分之后的具体步骤对于以上模块也会有交集部分。读者可以先跟着以下步骤先写代码,不断回顾写的代码和以上的思路框架的关系。
这里特别提醒读者,这篇技术文章仅仅介绍到绘制棋盘和正常开始游戏的部分,还远远没有达到完整的一个五子棋游戏,后续我会继续发这个五子棋游戏的文章。
首先我们我们要进行界面开发
设置窗体的相关内容,创建一个类,取名GoBangUI,并且创建一个方法,取名initGoUI
import javax.swing.*;
import java.awt.*;
public class GoBangUI{
public void initGoUI(){
//创建窗体对象
JFrame jf=new JFrame("五子棋");
//设置窗体的属性:尺寸,关闭选项,可视化
jf.setSize(900,900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
jf.setResizable(false);
jf.setLOcationRelativeTo(null);
}
public static void main(String[] args){
new GoBanUI().initGoUI();
}
}
这一步我们要绘制棋盘,另外创建一个类,取名棋盘ChessPanel并且继承JPanel
import javax.swing.*;
import java.awt.*;
public class ChessPanel extends JPanel{
//采用重写来使用JPanel的默认paint()方法,用以进行绘图
@override
public void paint(Graphics g){
//设置棋盘的底色为棕色,并且使用fillRect来填充整个面板
g.setColor(new Color(156, 139, 56));
g.fillRect(0,0,getWidth(),getHeight());
//绘制棋盘网格,黑色网格,设置网格线的坐标,长短,位置。以这个为基准线
g.setColor(Color.BLACK);
g.drawLine(50,50,50,550);
g.drawLine(50,50,550,50);
把这里设置的面板ChessPanel加到窗体上并且设置按钮的相关内容,左侧为控制面板右侧为游戏面板,这样使得整个界面更加美观
import javax.swing.*;
import java.awt.*;
public class GoBangUI{
public void initGoUI(){
//创建窗体对象
JFrame jf=new JFrame("五子棋");
//设置窗体的属性:尺寸,关闭选项,可视化
jf.setSize(900,900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//把网格能够实现在窗体上
ChessPanel chessPanel=new ChessPanel();
//按钮功能面板
JPanel btnPanel=new JPanel();
btnPanel.setBackground(Color.GRAY);
btnPanel.setPreferredSize(new Dimension(110,0));
jf.add(chessPanel,BorderLayout.CENTER);
jf.add(btnPanel,BorderLayout.WEST);
jf.setVisible(true);
jf.setResizable(false);
jf.setLOcationRelativeTo(null);
}
public static void main(String[] args){
new GoBanUI().initGoUI();
}
}
创建接口类,用以多个类都可以实现这个接口,并且有效降低可能存在的类与类的耦合,提高复用性,用接口更加清晰
public interface GoData {
//棋盘的固定数据的定义
int X=70;//棋盘网格的左上角X坐标
int Y=80;//棋盘网格的左上角Y坐标
int SIZE=40;//棋盘网格的尺寸,其实也就是棋子的尺寸
int ROWS=16;//棋盘的行数
int COLS=16;//棋盘的列数
int GRID_NUM=15;//棋盘的格子数
再找到ChessPanel,用implements来实现接口,在ChessPanel上用循环语句把棋盘绘制完整。drawLine() 方法被用于绘制棋盘的网格线。该方法可以在图形上下文中绘制两个指定点之间的线段
import javax.swing.*;
import java.awt.*;
public class ChessPanel extends JPanel implements GoData{
//采用重写来使用JPanel的默认paint()方法,用以进行绘图
@override
public void paint(Graphics g){
//设置棋盘的底色为棕色,并且使用fillRect来填充整个面板
g.setColor(new Color(156, 139, 56));
g.fillRect(0,0,getWidth(),getHeight());
//绘制棋盘网格,黑色网格,设置网格线的坐标,长短,位置
g.setColor(Color.BLACK);
//用循环语句,绘制完整棋盘
for(int i=0;i<ROWS;i++){
//画横线
g.drawLine(X,Y+i*SIZE,X+GRID_NUM*SIZE,Y+i*SIZE);
//画竖线
g.drawLine(X+i*SIZE,Y,X+i*SIZE,Y+GRID_NUM*SIZE);
}
用以初始化游戏界面的按钮面板,给四个按钮设置本文标签,并将这些按钮添加到btnPanel中。
import javax.swing.*;
public class GoBangUI{
public void initGoUI(){
//创建窗体对象
JFrame jf=new JFrame("五子棋");
//设置窗体的属性:尺寸,关闭选项,可视化
jf.setSize(900,900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//把网格能够实现在窗体上
ChessPanel chessPanel=new ChessPanel();
//按钮功能面板
JPanel btnPanel=new JPanel();
btnPanel.setBackground(Color.GRAY);
btnPanel.setPreferredSize(new Dimension(110,0));
jf.add(chessPanel,BorderLayout.CENTER);
jf.add(btnPanel,BorderLayout.WEST);
jf.setVisible(true);
jf.setResizable(false);
jf.setLOcationRelativeTo(null);
}
//初始化游戏界面的按钮面板
public void initBtnPanel(Jpanel btnPanel){
String[] strs={"开始游戏","悔棋“,"认输","退出游戏");//命名文本标签
for(int i=0;i<strs.length;i++){
//设置标签的诸多属性
JButton btn=new JButton(strs[i]);
btn.setBackGround(Color.WHITE);
btn.setPreferredSize(new Dimension(90,40));
btnPanel.add(btn);
}
}
public static void main(String[] args){
new GoBanUI().initGoUI();
}
}
再用this关键字调用initBtnPanel方法,并把参数btnPanel传递给这个方法
import javax.swing.*;
public class GoBangUI{
public void initGoUI(){
//创建窗体对象
JFrame jf=new JFrame("五子棋");
//设置窗体的属性:尺寸,关闭选项,可视化
jf.setSize(900,900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
//把网格能够实现在窗体上
ChessPanel chessPanel=new ChessPanel();
//按钮功能面板
JPanel btnPanel=new JPanel();
btnPanel.setBackground(Color.GRAY);
btnPanel.setPreferredSize(new Dimension(110,0));
//
this,initBtnPanel(btnPanel);
jf.add(chessPanel,BorderLayout.CENTER);
jf.add(btnPanel,BorderLayout.WEST);
jf.setVisible(true);
jf.setResizable(false);
jf.setLOcationRelativeTo(null);
}
//初始化游戏界面的按钮面板
public void initBtnPanel(Jpanel btnPanel){
String[] strs={"开始游戏","悔棋“,"认输","退出游戏");//命名文本标签
for(int i=0;i<strs.length;i++){
//设置标签的诸多属性
JButton btn=new JButton(strs[i]);
btn.setBackGround(Color.WHITE);
btn.setPreferredSize(new Dimension(90,40));
btnPanel.add(btn);
}
}
public static void main(String[] args){
new GoBanUI().initGoUI();
}
}
监听器部分,创建MouseAdapter,ActionListener,重写actionPerformed和mousePressed
再定义一个私有成员变量g,它是用来保存画笔对象的引用,再定义一个setGraphics方法,这个方法用来设置g的值
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class GoListener extends MouseAdapter implements ActionListener{
//用Graphics定义一个私有变量g,再用setGraphics方法设置g的值
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
@Override
public void actionPerformed(ActionEvent e){
}
@Override
public void mousePressed(MouseEvent e){
}
在mousePressed中获取x和y的坐标,并且用画圆的方法来画棋子
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class GoListener extends MouseAdapter implements ActionListener{
//用Graphics定义一个私有变量g,再用setGraphics方法设置g的值
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
@Override
public void actionPerformed(ActionEvent e){
System.out.println(e.getActionCommand());
}
@Override
public void mousePressed(MouseEvent e){
int x=e.getX();
int y=e.getY();
System.out.println("x:"+x+"y:"+y);
g.fillOval(x,y,SIZE,SIZE);
}
在GoBangUI上初始化监听器GoListener和鼠标监听器MouseListener,并把ActionListener监听器添加到按钮上。
这里我们注意一下,要把jf.setResizable和jf.setLocationRelativeTo移动到jf.setDefaultCloseOperation前面
然后再获取棋盘面板上的画笔对象,用goListener调用setGraphics
import javax.swing.*;
import java.awt.*;
public class GoBangUI {
GoListener goListener=new GoListener();//初始化监听器
public void initGoUI(){
//创建窗体对象
JFrame jf=new JFrame("五子棋");
//设置窗体的属性:尺寸,关闭选项,可视化
jf.setSize(900,900);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setResizable(false);
jf.setLOcationRelativeTo(null);
//把网格能够实现在窗体上
ChessPanel chessPanel=new ChessPanel();
chessPanel.addMouseListener(goListener);//棋盘面板实现鼠标监听
//按钮功能面板
JPanel btnPanel=new JPanel();
btnPanel.setBackground(Color.GRAY);
btnPanel.setPreferredSize(new Dimension(110,0));
//
this,initBtnPanel(btnPanel);
jf.add(chessPanel,BorderLayout.CENTER);
jf.add(btnPanel,BorderLayout.WEST);
jf.setVisible(true);
goListener.setGraphics(chessPanel.getGraphics());
}
//初始化游戏界面的按钮面板
public void initBtnPanel(Jpanel btnPanel){
String[] strs={"开始游戏","悔棋“,"认输","退出游戏");//命名文本标签
for(int i=0;i<strs.length;i++){
//设置标签的诸多属性
JButton btn=new JButton(strs[i]);
btn.setBackGround(Color.WHITE);
btn.setPreferredSize(new Dimension(90,40));
btnPanel.add(btn);
btn.addActionListener(goListener);//监听器添加到按钮上
}
}
public static void main(String[] args){
new GoBanUI().initGoUI();
}
}
接下来我们要设置棋子颜色,黑白都要有。现在GoListener中定义一个chessFlag。采用if循环来实现黑白交替下棋
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class GoListener extends MouseAdapter implements ActionListener{
//用Graphics定义一个私有变量g,再用setGraphics方法设置g的值
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
int chessFlag=1;棋子颜色标识,1代表黑棋,2代表白棋,0代表不能下棋
@Override
public void actionPerformed(ActionEvent e){
System.out.println(e.getActionCommand());
}
@Override
public void mousePressed(MouseEvent e){
int x=e.getX();
int y=e.getY();
System.out.println("x:"+x+"y:"+y);
//用循环实现黑白交替,每次循环都给chessFlag赋一个黑白交替,1执行完循环就重新赋值为2
if(chessFlag==1){
g.setColor(Color.BLACK);
chessFlag=2;
}else if(chessFlag==2){
g.setColor(Color.WHITE);
chessFlag=1;
}
g.fillOval(x,y,SIZE,SIZE);
}
}
接下来实现要实现棋子在别处下子,系统会提示“此处不能下棋”
先定义两个变量r和c,先打印一下r和c的坐标,让程序员可以看到具体数据
然后接着在下面写一个判断语句,来判断哪些位置不能下棋
再利用前面定义的c和r还原标准坐标
并把X-SIZE/2和Y-SIZE/2的判断加到设置弹窗的if语句中
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class GoListener extends MouseAdapter implements ActionListener{
//用Graphics定义一个私有变量g,再用setGraphics方法设置g的值
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
int chessFlag=1;棋子颜色标识,1代表黑棋,2代表白棋,0代表不能下棋
@Override
public void actionPerformed(ActionEvent e){
System.out.println(e.getActionCommand());
}
@Override
public void mousePressed(MouseEvent e){
int x=e.getX();
int y=e.getY();
System.out.println("x:"+x+"y:"+y);
//计算鼠标点击位置的行列值
int r=(y-Y+SIZE/2)/SIZE;
int c=(x-X+SIZE/2)/SIZE;
System.out.println("r:"+r+"c:"+c);
//判断下棋的范围,这里提示一下,逻辑或运算,一个判断式成立,则整个式子即成立
if(x<=X-SIZE/2||y<=Y-SIZE/2||r<0||r>=ROWS||c<0||C>=COLS){
//设置提示弹窗
JOptionPane.showMessageDialog(null,"此处不能下棋");
return;
}
//用循环实现黑白交替,每次循环都给chessFlag赋一个黑白交替,1执行完循环就重新赋值为2
if(chessFlag==1){
g.setColor(Color.BLACK);
chessFlag=2;
}else if(chessFlag==2){
g.setColor(Color.WHITE);
chessFlag=1;
}
//利用行列值还原标准坐标
int cx=c*SIZE+X-SIZE/2;
int cy=r*SIZE+Y-SIZE/2;
g.fillOval(x,y,SIZE,SIZE);
}
}
接下来我们解决棋子重复落子的问题,这一块内容严格来讲,是属于数据存储部分,即第四步。首先我们可以来思考一下,棋子落下的位置,可以用什么模型来存储位置数据呢?
可以尝试用二维数组,在方法setGraphics下面定义一个二维数组,并在弹窗if语句和黑白交替下棋if语句中,写入chessArray[r][c]=chessFlag用以存入二维数组
再将这个矩阵打印出来便于观察,定义一个方法printChessArray
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class GoListener extends MouseAdapter implements ActionListener{
//用Graphics定义一个私有变量g,再用setGraphics方法设置g的值
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
//存储棋子的二维数组,用int类型的二维数组,int类型的默认初始值是0
int[][] chessArray=new int[ROWS][COLS];
int chessFlag=1;棋子颜色标识,1代表黑棋,2代表白棋,0代表不能下棋
@Override
public void actionPerformed(ActionEvent e){
System.out.println(e.getActionCommand());
}
@Override
public void mousePressed(MouseEvent e){
int x=e.getX();
int y=e.getY();
System.out.println("x:"+x+"y:"+y);
//计算鼠标点击位置的行列值
int r=(y-Y+SIZE/2)/SIZE;
int c=(x-X+SIZE/2)/SIZE;
System.out.println("r:"+r+"c:"+c);
//判断下棋的范围,这里提示一下,逻辑或运算,一个判断式成立,则整个式子即成立
if(x<=X-SIZE/2||y<=Y-SIZE/2||r<0||r>=ROWS||c<0||C>=COLS){
//设置提示弹窗
JOptionPane.showMessageDialog(null,"此处不能下棋");
return;
}
if(chessArray[r][c]≠≠0){
JOptionPane.showMessageDialog(null,"此处已经有棋子");
//把棋子标识放入二维数组
chessArray[r][c]=chessFlag;
//用循环实现黑白交替,每次循环都给chessFlag赋一个黑白交替,1执行完循环就重新赋值为2
if(chessFlag==1){
g.setColor(Color.BLACK);
chessFlag=2;
}else if(chessFlag==2){
g.setColor(Color.WHITE);
chessFlag=1;
}
//利用行列值还原标准坐标
int cx=c*SIZE+X-SIZE/2;
int cy=r*SIZE+Y-SIZE/2;
g.fillOval(x,y,SIZE,SIZE);
printChessArray();
}
//定义方法来观察矩阵变化
public void printChessArray(){
for(int i=0;j<chessArray.length;i++){
for(int j=0;j<chessArray[i].length;j++){
System.out.print(chessArray[i][j]+"");
}
}
但是现在有一个问题,就是当最小化界面后,棋子会消失,但是棋子在的位置还是无法下棋,这表明矩阵的数据还是存储不变的。但这并不是我们希望看到的,因为我们用户使用程序,最小化之后再显示出来的话,希望界面是不变的。这里我们就要用到重绘了
我们把二维数组的棋子遍历出来,根据矩阵中的数据重新绘制棋子,private修饰一下二维数组,在类ChessPanel中创建方法即可
import javax.swing.*;
import java.awt.*;
public class ChessPanel extends JPanel implements GoData {
private int[][] chessArray;
public void setChessArray(int[][] chessArray){
this.chessArray=chessArray;
}
……
}
然后需要在GoBangUI类中声明
chessPanel.setChessArray(goListener.getChessArray());
并在GoListener中,存储棋子的二维数组,用private修饰
public class GoListener extends MouseAdapter implements ActionListener, GoData {
//把二维数组的棋子遍历出来,依据存储的行列下标绘制棋子
private Graphics g;
public void setGraphics(Graphics g){this.g=g;}
//存储棋子的二维数组
private int[][] chessArray=new int[ROWS][COLS];
public int[][] getChessArray(){
return chessArray;
}
在chessPanel中的paint方法中,写下重绘棋子的方法,用length获取数组的长度,行数和列数都获取到,具体循环语句如下:
for (int i = 0; i < chessArray.length; i++) {
for (int j = 0; j < chessArray[i].length; j++) {
int chessNum = chessArray[i][j];
if (chessNum != 0) {
g.setColor(chessNum != 1 ? Color.WHITE : Color.BLACK);
int cx = X + j * SIZE - SIZE / 2;
int cy = Y + i * SIZE - SIZE / 2;
g.fillOval(cx, cy, SIZE, SIZE);
}
}
}
接下来我们要设置点击按钮可以开始游戏,点击开始游戏按钮-棋盘可以下棋并同时按钮变为“结束游戏”
对于我们定义的chessFlag,用if循环语句即可,写在监听器类GoListener中的方法mousePressed中
if(chessFlag==0){
JOptionPane.showMessageDialog(null,"请点击开始游戏按钮");
return;
}
然后再在监听器类的actionperformed方法中写入按钮文本变换的代码
public void actionPerformed(ActionEvent e) {
//获取按钮上的字符串
String action=e.getActionCommand();
//获取按钮对象
Object source=e.getSource();
//转型成按钮对象
JButton btn=(JButton) source;
if(action.equals("开始游戏")){
chessFlag=1;
//把按钮的文本修改为:结束游戏
btn.setText("结束游戏");
}else if(action.equals("结束游戏")){
chessFlag=0;
btn.setText("开始游戏");
继续在Golistener类中写下方法clearChessArray,用于用户点击结束游戏之后,实现棋盘的清空
在Golistener类的actionPerformed方法中调用这个clearChessArray方法,用this关键字
public void clearChessArray(){
for(int i=0;i<chessArray.length;i++){
for(int j=0;j<chessArray[i].length;j++){
chessArray[i][j]=0;
}
}
}
public void actionPerformed(ActionEvent e) {
//获取按钮上的字符串
String action=e.getActionCommand();
//获取按钮对象
Object source=e.getSource();
//转型成按钮对象
JButton btn=(JButton) source;
if(action.equals("开始游戏")){
chessFlag=1;
//把按钮的文本修改为:结束游戏
btn.setText("结束游戏");
}else if(action.equals("结束游戏")){
chessFlag=0;
btn.setText("开始游戏");
/*接下来要做的是清空棋盘
1.删除二维数组中的数据
2.刷新面板
*/
this.clearChessArray();//调用clearChessArray方法
chessPanel.paint(g);
}
}
再在GoListener类中定义一个对象,取名为chessPanel
随后定义一个方法,取名为setChessPanel
private ChessPanel chessPanel;
public void setChessPanel(ChessPanel chessPanel){
this.chessPanel=chessPanel;
}
并在界面类中调用这个方法,写入goListener.setChessPanel(chessPanel);
再在this.clearChessArray();后面写入棋盘的重绘,即chessPanel.paintChessPanel(g);
这样我们就可以实现点击按钮结束游戏,棋盘清空,实现重绘
然后我们开始设置点击退出游戏按钮,界面消失
在actionperformed方法中再写一个if语句,并用System.exit来实现
public void actionPerformed(ActionEvent e) {
//获取按钮上的字符串
String action=e.getActionCommand();
//获取按钮对象
Object source=e.getSource();
//转型成按钮对象
JButton btn=(JButton) source;
if(action.equals("开始游戏")){
chessFlag=1;
//把按钮的文本修改为:结束游戏
btn.setText("结束游戏");
}else if(action.equals("结束游戏")){
chessFlag=0;
btn.setText("开始游戏");
/*接下来要做的是清空棋盘
1.删除二维数组中的数据
2.刷新面板
*/
this.clearChessArray();//调用clearChessArray方法
chessPanel.paint(g);
}else if(action.equals("退出游戏")){
System.exit(0);
}
}
以上流程和代码实际操作就实现了游戏界面的设计,落子下棋,开始游戏——结束游戏,退出游戏这些环节,后续的悔棋,判断输赢的内容会在随后的文章介绍到
启发
读者可以先理顺思路,代码部分其实还可以继续优化,读者可以自行思考,或者在这篇文章的代码的基础上优化,或者增加自己想要的功能。