一、编写五子棋的基本思路
1、游戏界面开发;
2、绘制棋盘,棋子,提示信息;
3、鼠标监听,需要保证棋子下在交叉线上;
4、胜负判定
5、按钮功能实现
6、计时器功能实现
二、以下是程序源代码:
1、游戏界面开发
//界面开发
public FiveChessFrame() {
//设置界面标题和大小
this.setTitle("五子棋");
this.setSize(1280, 760);
//保证游戏界面在桌面中心
this.setLocation((width - 1280) / 2, (height - 720) / 2);
// 窗体不可伸缩的
this.setResizable(false);
//界面关闭方式
this.setDefaultCloseOperation(3);
//添加鼠标监听
this.addMouseListener(this);
//用ImageIO读取背景图片文件
try {
bgImage = ImageIO.read(new File("beijing2.jpg"));
} catch (IOException e) {
e.printStackTrace();
}
//窗体可视
this.setVisible(true);
// 线程启动
t.start();
// 线程挂起
t.suspend();
// 刷新屏幕,防止开始游戏时无法显示(黑屏)
this.repaint();
}
2、绘制棋盘,棋子,提示信息
在这一模块中,主要使用了画笔(Graphics g)对棋盘、棋子绘制和运用双缓冲技术防止屏幕闪烁。在这一部分的主要难点在于对棋子的保存,让每下一个棋子都保留在棋盘上;而解决的思路是:用一个数组来保存棋盘上每个交点的数据,每下一棋,则改变此棋子为位置在数组中的数据;最后只需要每下一棋,就通过遍历数组就可以画出之前下过的棋子了,由此达到保存棋子的功能。
而双缓冲技术尚是初次接触,其核心思想是:把之前棋盘上的棋局合成一张图像,每次落子时调用paint方法只需要把图像画出就可以了,这样做可以避免当棋子多时,因为一个一个地画棋子形成的延时,在视觉上看就像是屏幕闪烁一样的问题。
//使用画笔绘制棋盘,棋子等
// 鼠标点击坐标
int x = 0;
int y = 0;
// 用二维数组保存下过的棋子数据(0没有棋子,1是黑子,2是白子)
int[][] allchess = new int[29][17];
// 提示当前下棋的是黑还是白
String message = "黑方先行";
// 保存双方剩余时间
String blackMessage = "无限制";
String whiteMessage = "无限制";
//画笔
public void paint(Graphics g) {
// 双缓冲技术防止屏幕闪烁(大致原理为:把之前的面板当做一张图片画出,避免了工作量大时棋子画不过来)
BufferedImage bufferedImage = new BufferedImage(1280, 760, BufferedImage.TYPE_INT_RGB);
Graphics g1 = bufferedImage.createGraphics();
// 画背景,
g1.drawImage(bgImage, 0, 35, this);
g1.setFont(new Font("微软雅黑", Font.BOLD, 30));
g1.drawString("欢乐五子棋", 485, 70);
g1.setFont(new Font("宋体", Font.BOLD, 40));
//提示该谁落子了
g1.drawOval(435, 680, 200, 65);
g1.drawString(message, 450, 725);
//倒计时提示款
g1.setFont(new Font("黑体", Font.BOLD, 25));
g1.drawString("黑方时间:" + blackMessage, 95, 725);
g1.drawString("白方时间:" + whiteMessage, 710, 725);
// 画棋盘
// 画横线
for (int i = 0; i < 17; i++) {
g1.drawLine(20, 90 + 35 * i, 1002, 90 + 35 * i);
}
// 画竖线
for (int j = 0; j < 29; j++) {
g1.drawLine(20 + 35 * j, 90, 20 + 35 * j, 650);
}
// 标注四个点
g1.fillOval(157, 192, 7, 7);
g1.fillOval(857, 192, 7, 7);
g1.fillOval(157, 542, 7, 7);
g1.fillOval(857, 542, 7, 7);
// 中心点
g1.fillOval(507, 367, 7, 7);
// 四条边的中点
g1.fillOval(507, 193, 7, 7);
g1.fillOval(507, 542, 7, 7);
g1.fillOval(157, 367, 7, 7);
g1.fillOval(857, 367, 7, 7);
// 绘制全部棋子
for (int i = 0; i < 29; i++) {
for (int j = 0; j < 17; j++) {
// 下过的黑子一一画出
if (allchess[i][j] == 1) {
int tempx = i * 35 + 20;
int tempy = j * 35 + 90;
g1.fillOval(tempx - 10, tempy - 10, 20, 20);
}
// 下过的白子一一画出
if (allchess[i][j] == 2) {
int tempx = i * 35 + 20;
int tempy = j * 35 + 90;
g1.setColor(Color.WHITE);
g1.fillOval(tempx - 10, tempy - 10, 20, 20);
g1.setColor(Color.BLACK);
g1.drawOval(tempx - 10, tempy - 10, 20, 20);
}
}
}
//把面板内容当初一张图片画出
g.drawImage(bufferedImage, 0, 0, this);
}
3、鼠标监听,按键功能实现
在这一模块中,因为背景的原因,没有使用JButton来构建按钮,而是通过感应鼠标点击在背景中按钮的区域来达到类似按钮的功能。其中的难点在于悔棋功能的实现,而思路是:通过每下一个棋子就记录下棋子的位置,而悔棋只需要把之前数组中棋子位置的数据改为0(0是没棋子),把先行提示改回来,再调用repaint()重绘就可以了。
// 标识当前棋子的颜色
boolean isBlack = true;
// 标识当前游戏是否可以继续
boolean canplay = true;
// 记录每一个棋子的位置
int chessX;
int chessY;
// 保存最多拥有多少时间(秒)
int maxtime = 0;
// 做倒计时的线程
Thread t = new Thread(this);
// 保存黑方和白方剩余时间
int blacktime = 0;
int whitetime = 0;
@Override
public void mousePressed(MouseEvent e) {
if (canplay == true) {
// 获取点击位置
x = e.getX();
y = e.getY();
// 判断鼠标是否点在棋盘上
if (x >= 20 && x <= 1005 && y >= 90 && y <= 655) {
// 保证输出的棋子在点上(说实话,这种方法下的棋子落点不够准确)
x = (x - 20) / 35;
y = (y - 90) / 35;
// 判断位置是否可以下棋(空的)
if (allchess[x][y] == 0) {
// 判断当前下的是什么棋子
if (isBlack == true) {
// 保存棋子
allchess[x][y] = 1;
//保存刚下棋子的位置
chessX = x;
chessY = y;
isBlack = false;
message = "白方先行";
} else {
allchess[x][y] = 2;
chessX = x;
chessY = y;
isBlack = true;
message = "黑方先行";
}
} else {
return;
}
// 每下一棋都重新画棋盘
this.repaint();
// 每一步都判断胜负
boolean winFlag = this.checkWin();
if (winFlag == true) {
JOptionPane.showMessageDialog(this, "游戏结束," + (allchess[x][y] == 1 ? "黑方" : "白方") + "获胜!");
//锁棋盘,不再允许落子
canplay = false;
}
}
}
// 开始游戏按钮
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 95 && e.getY() <= 135) {
//提示弹框
int result = JOptionPane.showConfirmDialog(this, "是否重新开始游戏?");
//点击是
if (result == 0) {
JOptionPane.showMessageDialog(this, "重新开始游戏");
canplay = true;
// 清空棋盘(即把allchess数据清0,游戏信息改正,标识改正)
allchess = new int[29][17];
message = "黑方先行";
isBlack = true;
if (maxtime > 0) {
blackMessage = maxtime / 3600 + ":" + (maxtime / 60 - maxtime / 3600 * 60) + ":"
+ (maxtime - maxtime / 60 * 60);
whiteMessage = maxtime / 3600 + ":" + (maxtime / 60 - maxtime / 3600 * 60) + ":"
+ (maxtime - maxtime / 60 * 60);
// 重新启动线程
t.resume();
} else {
blackMessage = "无限制";
whiteMessage = "无限制";
}
this.repaint();
}
}
// 游戏设置(时间)
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 170 && e.getY() <= 210) {
String input = JOptionPane.showInputDialog("请输入游戏的最大时间(分钟)");
try {
maxtime = Integer.parseInt(input) * 60;
if (maxtime < 0) {
JOptionPane.showMessageDialog(this, "请输入正数");
}
if (maxtime == 0) {
int result = JOptionPane.showConfirmDialog(this, "设置完成,是否重新开始游戏?");
if (result == 0) {
allchess = new int[29][17];
message = "黑方先行";
isBlack = true;
blacktime = maxtime;
whitetime = maxtime;
blackMessage = "无限制";
whiteMessage = "无限制";
this.repaint();
}
}
if (maxtime > 0) {
int result = JOptionPane.showConfirmDialog(this, "设置完成,是否重新开始游戏?");
if (result == 0) {
allchess = new int[29][17];
message = "黑方先行";
isBlack = true;
//把剩余的时间赋给二者
blacktime = maxtime;
whitetime = maxtime;
blackMessage = maxtime / 3600 + ":" + (maxtime / 60 - maxtime / 3600 * 60) + ":"
+ (maxtime - maxtime / 60 * 60);
whiteMessage = maxtime / 3600 + ":" + (maxtime / 60 - maxtime / 3600 * 60) + ":"
+ (maxtime - maxtime / 60 * 60);
t.resume();
this.repaint();
}
}
} catch (NumberFormatException e1) {
JOptionPane.showMessageDialog(this, "请规范输入时间!");
}
}
// 游戏说明
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 255 && e.getY() <= 295) {
JOptionPane.showMessageDialog(this, "此处没有说明。");
}
// 认输
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 395 && e.getY() <= 435) {
int result = JOptionPane.showConfirmDialog(this, "是否确认认输?");
if (result == 0) {
if (isBlack) {
JOptionPane.showMessageDialog(this, "黑方认输,游戏结束");
} else {
JOptionPane.showMessageDialog(this, "白方认输,游戏结束");
}
canplay = false;
}
}
// 悔棋
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 470 && e.getY() <= 515) {
int result = JOptionPane.showConfirmDialog(this, (isBlack == true ? "白方悔棋,黑方是否同意?" : "黑方悔棋,白方是否同意?"));
if (result == 0) {
JOptionPane.showMessageDialog(this, "对方允许悔棋,请重新落子。");
//把刚下的棋子数据清0
allchess[chessX][chessY] = 0;
//改变先行提示
if (isBlack == true) {
isBlack = false;
message = "白方先行";
} else {
isBlack = true;
message = "黑方先行";
}
//重画棋局
this.repaint();
}
if (result == 1) {
JOptionPane.showMessageDialog(this, "对方不允许你悔棋,请继续游戏。");
}
}
// 退出
if (e.getX() >= 1080 && e.getX() <= 1240 && e.getY() >= 555 && e.getY() <= 600) {
JOptionPane.showMessageDialog(this, "游戏结束");
//直接退出
System.exit(0);
}
}
4、胜负判定
在此模块中,通过五子棋的规则(横、竖、左斜、右斜任一方向上5个棋子连成一线)来判断胜负;而计数不仅要往右计数,还要往左计算,通过横纵坐标的加减来判断右边后,根据保存刚下棋子的坐标来重新向左边计数。
// 胜负判定(四个方向)
private boolean checkWin() {
boolean flag = false;
// 保存共有相同颜色相连棋子的个数
int count = 1;
// 棋子用颜色标识,1是黑子,2是白子
int color = allchess[x][y];
// 判断横向
count = this.checkCount(1, 0, color);
if (count >= 5) {
flag = true;
} else {
// 判断纵向
count = this.checkCount(0, 1, color);
if (count >= 5) {
flag = true;
} else {
// 判断45°
count = this.checkCount(1, -1, color);
if (count >= 5) {
flag = true;
} else {
// 判断135°
count = this.checkCount(1, 1, color);
if (count >= 5) {
flag = true;
}
}
}
}
return flag;
}
// 判定是否有5个棋子相连
public int checkCount(int xChange, int yChange, int color) {
int count = 1;
// 保存刚下的棋子坐标
int tempx = xChange;
int tempy = yChange;
//从刚下的棋子坐标开始往右边判断
// 每次向前移动一次,遇到同一颜色就继续判断
while (x + xChange >= 0 && x + xChange < 29 && y + yChange >= 0 && y + yChange < 17
&& color == allchess[x + xChange][y + yChange]) {
count++;
if (xChange != 0)
xChange++;
if (yChange != 0) {
if (yChange > 0) {
// 使棋子沿着右下一条线移动,进行判断
yChange++;
} else {
// 使棋子沿着右上一条线移动,进行判断
yChange--;
}
}
}
//从刚下的棋子坐标开始往左边判断
xChange = tempx;
yChange = tempy;
while (x - xChange >= 0 && x - xChange < 29 && y - yChange >= 0 && y - yChange < 17
&& color == allchess[x - xChange][y - yChange]) {
count++;
if (xChange != 0) {
xChange++;
}
if (yChange != 0) {
if (yChange > 0) {
// 使棋子沿着左上一条线移动,进行判断
yChange++;
} else {
// 使棋子沿着左下一条线移动,进行判断
yChange--;
}
}
}
return count;
}
5、计时器功能实现
@Override
public void run() {
//还有剩余时间,则开始计时
if (maxtime > 0) {
while (true) {
if (isBlack) {
blacktime--;
if (blacktime == 0) {
JOptionPane.showMessageDialog(this, "黑方超时,游戏结束!");
canplay = false;
//线程挂起,停止计时
t.suspend();
}
} else {
whitetime--;
if (whitetime == 0) {
JOptionPane.showMessageDialog(this, "白方超时,游戏结束!");
canplay = false;
t.suspend();
}
}
// 睡眠1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 通过线程的运行、睡眠来间断显示blackMessage或whiteMessage来达到时间跳动的效果
blackMessage = blacktime / 3600 + ":" + (blacktime / 60 - blacktime / 3600 * 60) + ":"
+ (blacktime - blacktime / 60 * 60);
whiteMessage = whitetime / 3600 + ":" + (whitetime / 60 - whitetime / 3600 * 60) + ":"
+ (whitetime - whitetime / 60 * 60);
this.repaint();
}
}
}
项目总结:
在整个五子棋项目中,最让我印象深刻的是悔棋功能的事现;尽管我的思路是对的,但是老是没有找准棋子的坐标位置,甚至一度把我上一次鼠标点击的位置(x,y)当成了棋子的坐标,实际上上一次点击的位置是悔棋按钮,因此老是出现数组越界,更是揪着数组越界这个错误,又改成棋子数组的下标((x - 20) / 35, (y - 90) / 35);一番周折后才意识到问题的所在,才重新定义变量来保持棋子位置,悔棋功能得以实现。此外,在整个项目中,我对于通过JOptionPane设置弹框,通过 BufferedImage.createGraphics()实现双缓冲,通过线程的start()和suspend()、sleep()来达到计时的效果,通过标识的运用来减小代码量等等也有所收获。
此外,在这个项目里,我也遇到了一些问题,比如使用了双缓冲技术后,我发现我的棋盘,时间计时,先行提示信息等一些通过双缓冲后的画笔画的东西都由黑色变成了白色,而且一开始下的几个黑棋也变白了;对于这个问题,不知所以然。