

老Java程序员花两天做了个消消乐(天天爱消除)
source link: https://blog.csdn.net/dkm123456/article/details/118079900
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

老Java程序员花两天做了个消消乐(天天爱消除)
老Java程序员花两天做了个消消乐(天天爱消除)
一直就想做一个消消乐,这次正好找到了素材,就自己琢磨写了一个,我觉得这个游戏难点就在消除、以及消除后的下落,其他的地方也就还好,这次做完了写个文章大家唠一波。
1.绘制窗口、按钮、边框等。
2.实现Card类,用来代表每一个小图形。
3.创建下标集合,因图片下标是0-5,所以用随机函数随机出下标,用来代表不同的图形,并依次添加打集合indexs中。
4.对此集合进行随机排序处理。
5.创建二维数组9行8列,根据集合的下标和二维数组对应的下标实例化各个卡片,并对应放在二维数组中。
6.设定卡片交换—当前卡片的上下左右才能交换。
7.判断横向、纵向是否超过3个相同的,是则消除。
8.消除后对应的卡片下落。
首先创建一个游戏窗体类GameFrame,继承至JFrame,用来显示在屏幕上(window的对象),每个游戏都有一个窗口,设置好窗口标题、尺寸、布局等就可以。
/*
* 游戏窗体类
*/
public class GameFrame extends JFrame {
public GameFrame() {
setTitle("消消乐");//设置标题
setSize(386, 440);//设定尺寸
setLayout(new BorderLayout());
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);//点击关闭按钮是关闭程序
setLocationRelativeTo(null); //设置居中
setResizable(false); //不允许修改界面大小
}
}
创建面板容器GamePanel继承至JPanel
package main;
import java.awt.Graphics;
import javax.swing.JFrame;
import javax.swing.JPanel;
/*
* 画布类
*/
public class GamePanel extends JPanel{
GamePanel gamePanel=this;
private JFrame mainFrame=null;
//构造里面初始化相关参数
public GamePanel(JFrame frame){
this.setLayout(null);
mainFrame = frame;
mainFrame.setVisible(true);
}
@Override
public void paint(Graphics g) {
}
}
再创建一个Main类,来启动这个窗口,用来启动。
public class Main {
//主类
public static void main(String[] args) {
GameFrame frame = new GameFrame();
GamePanel panel = new GamePanel(frame);
frame.add(panel);
frame.setVisible(true);//设定显示
}
}
右键执行这个Main类,窗口建出来了
创建菜单及菜单选项
创建菜单
//初始化按钮
private void initMenu(){
// 创建菜单及菜单选项
jmb = new JMenuBar();
JMenu jm1 = new JMenu("游戏");
jm1.setFont(new Font("黑体", Font.BOLD, 15));// 设置菜单显示的字体
JMenu jm2 = new JMenu("帮助");
jm2.setFont(new Font("黑体", Font.BOLD, 15));// 设置菜单显示的字体
JMenuItem jmi1 = new JMenuItem("开始新游戏");
JMenuItem jmi2 = new JMenuItem("退出");
jmi1.setFont(new Font("黑体", Font.BOLD, 15));
jmi2.setFont(new Font("黑体", Font.BOLD, 15));
JMenuItem jmi3 = new JMenuItem("操作说明");
jmi3.setFont(new Font("黑体", Font.BOLD, 15));
JMenuItem jmi4 = new JMenuItem("胜利条件");
jmi4.setFont(new Font("黑体", Font.BOLD, 15));
jm1.add(jmi1);
jm1.add(jmi2);
jm2.add(jmi3);
jm2.add(jmi4);
jmb.add(jm1);
jmb.add(jm2);
mainFrame.setJMenuBar(jmb);// 菜单Bar放到JFrame上
jmi1.addActionListener(this);
jmi1.setActionCommand("Restart");
jmi2.addActionListener(this);
jmi2.setActionCommand("Exit");
jmi3.addActionListener(this);
jmi3.setActionCommand("help");
jmi4.addActionListener(this);
jmi4.setActionCommand("win");
}
实现ActionListener并重写方法actionPerformed
actionPerformed方法的实现
@Override
public void actionPerformed(ActionEvent e) {
String command = e.getActionCommand();
UIManager.put("OptionPane.buttonFont", new FontUIResource(new Font("宋体", Font.ITALIC, 18)));
UIManager.put("OptionPane.messageFont", new FontUIResource(new Font("宋体", Font.ITALIC, 18)));
if ("Exit".equals(command)) {
Object[] options = { "确定", "取消" };
int response = JOptionPane.showOptionDialog(this, "您确认要退出吗", "",
JOptionPane.YES_OPTION, JOptionPane.QUESTION_MESSAGE, null,
options, options[0]);
if (response == 0) {
System.exit(0);
}
}else if("Restart".equals(command)){
if(!"end".equals(gamePanel.gameFlag)){
JOptionPane.showMessageDialog(null, "正在游戏中无法重新开始!",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}else {
restart();
}
}else if("help".equals(command)){
JOptionPane.showMessageDialog(null, "鼠标点击选中后,与相邻的切换,超过3个成行或者成列则消除!",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}else if("win".equals(command)){
JOptionPane.showMessageDialog(null, "300秒3000分胜利,否则失败!",
"提示!", JOptionPane.INFORMATION_MESSAGE);
}
}
初始化图片
将所有要用到的图片初始化,方便待会使用
public class ImageValue {
//小卡片
public static List<BufferedImage> itemImageList = new ArrayList<BufferedImage>();
//路径
public static String ImagePath = "/images/";
//将图片初始化
public static void init(){
String path = "";
//图片初始化
for(int i=0;i<=5;i++){
try {
path = ImagePath +"tile_"+ i+".png";
itemImageList.add(ImageIO.read(ImageValue.class.getResource(path)));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
初始化下标集合
1.初始化下标值,随机从6张图片中选取,下标[0-5]。
2.8列9行,当集合的长度满足72则跳出while循环。
3.使用 Collections.shuffle 对集合进行随机排序(其实不排序也行,本身也是随机来的)。
//随机排序
private void sortImage() {
Collections.shuffle(indexs);
}
//初始化下标值
private void initIndexs() {
Random random = new Random();
int n ;
while(true){//
n = random.nextInt(6);//随机从6张图片下标中选取[0-5]
indexs.add(n);
if(indexs.size()==72){
break;
}
}
}
drawImage介绍
此例中,因为图片是合在一起的,需要裁剪
所以要用以下方法:
g.drawImage(img, dx1, dy1, dx2, dy2, sx1, sy1, sx2, sy2, observer)
其中img是Image图片对象,而d开头的都是destinetiong,s开头的是source。
也就是说分别定义好来源和目标的两个矩形的左上角和右下角的点,它会自动帮你剪裁和适应。
public class Card {
private int x = 0;//对应行
private int y = 0;//对应列
private int dx = 0;//图形显示左上角x位置
private int odx = 0;//图形更新后显示左上角x位置
private int dy = 0;//图形显示左上角y位置
private int ody = 0;//图形更新后显示左上角y位置
private int dir = 1;//方向
private int width =32;//宽
private int height = 32;//高
private int pIndex = 0;//对应素材图片下标
private int index = 0;//对应图片下标值
private int type = 1;//1:10张的 2:20张的
private BufferedImage image = null;//图片对象
private GamePanel panel=null;//GamePanel
private boolean alive=true;//是否存活
private boolean selected = false;//是否选中
private int moveFlag=0;//移动标示 0 不移动 1 横向移动 2纵向移动
private int speed=15;//移动速度
public Card(int x,int y,int pIndex,GamePanel panel){
this.x=x;
this.y=y;
this.dx = 40+y*(32+3)+10;
this.dy = 35+x*(32+3)+10;
this.panel=panel;
this.pIndex=pIndex;
this.image = ImageValue.itemImageList.get(pIndex);
}
//绘制
public void draw(Graphics g) {
int index = this.index;
//index 默认是0,就是从图片中截取第一个
int sx1 = index*32;
int sy1 = 0;
//截取的右下角计算
int sx2 = (index+1)*32;
int sy2 = 32;
g.drawImage(this.image,dx, dy,dx+width,dy+height,sx1,sy1,sx2,sy2 ,null );
}
}
创建一个Card实例,并将它设置给二维数组第一个元素
//初始化卡片
private void initCards() {
Card card = new Card(0, 0, 0, this);
cards[0][0]=card;
}
paint方法绘制一个边框,并把二维数组的card绘制
@Override
public void paint(Graphics g) {
super.paint(g);
//绘制边框
BasicStroke bs_2=new BasicStroke(3,BasicStroke.CAP_ROUND,BasicStroke.JOIN_MITER);
Graphics2D g_2d=(Graphics2D)g;
g_2d.setColor(new Color(0,191,255));
g_2d.setStroke(bs_2);
g_2d.drawRect(38, 32, 305, 334);
Card card;
for (int i = 0; i <ROWS; i++) {
for (int j = 0; j < COLS; j++) {
card = cards[i][j];
if(card!=null){
card.draw(g);
}
}
}
在Card类中加入线程,更新index,因为index的变更会更新裁剪位置,此方法要在构造函数中调用。
private void rock() {
new Thread(new Runnable() {
@Override
public void run() {
while (alive) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
index++;
if(index==10){
index=0;
}
}
}
}).start();
}
在GamePanel中开启主线程,用来repaint,即可看到动画。
private class RefreshThread implements Runnable {
@Override
public void run() {
while (true) {
if ("start".equals(gameFlag)) {
repaint();
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
}
}
}
启动线程
gameFlag="start";
//主线程启动
mainThread = new Thread(new RefreshThread());
mainThread.start();
将卡片补齐,修改initCards方法
//初始化卡片
private void initCards() {
Card card;
int index = 0 ;
int temp=0;
for (int i = 0; i <ROWS; i++) {
for (int j = 0; j < COLS; j++) {
temp = Integer.valueOf(String.valueOf(indexs.get(index)));
card = new Card(i, j, temp, this);
cards[i][j]=card;
index++;
}
}
}
此时效果:
加入点击事件
Card类中加入isPoint方法用来判断鼠标点击是否在范围内。
//判断鼠标是否卡片范围内
boolean isPoint(int x,int y){
//大于左上角,小于右下角的坐标则肯定在范围内
if(x>this.dx && y >this.dy
&& x<this.dx+this.width && y <this.dy+this.height){
return true;
}
return false;
}
GamePanel加入方法判断交换的位置,必须在其上下方、或者左右方才允许交换。(当然如果选择交换的本身是一样的,则同样不允许交换)
//相邻才能交换
private int checkTran(Card card) {
if(card.getpIndex()==curCard.getpIndex()){//相同的不交换
return 4;
}
int x = curCard.getX();
int y = curCard.getY();
int x1 = card.getX();
int y1 = card.getY();
if(y==y1){//在上下
if(x1+1==x||x1-1==x){
return 2;
}
}
if(x==x1){//在左右
if(y1+1==y||y1-1==y){
return 1;
}
}
return 0;
}
事件代码,tran的方法先写个空的,等会来写
//鼠标事件的创建
private void createMouseListener() {
MouseAdapter mouseAdapter = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if(!"start".equals(gameFlag)) return ;
int x = e.getX();
int y = e.getY();
Card card;
for (int i = 0; i <ROWS; i++) {
for (int j = 0; j < COLS; j++) {
card = cards[i][j];
if(card==null)continue;
if(card.isPoint(x, y)){
MusicPlayer.chooseMisic();
if(curCard==null){
curCard = card ;
card.setSelected(true);
}else {
int dir= checkTran(card);
if(dir!=0&&dir!=4){//相邻才能交换
tran(card,dir);
}else {//不是相邻则当前取消选择
curCard.setSelected(false);
card.setSelected(true);
curCard = card ;
}
}
return ;//直接跳出
}
}
}
}
};
addMouseMotionListener(mouseAdapter);
addMouseListener(mouseAdapter);
}
tran方法实现
1.横向交换—交换其Y值
2.纵向交换—交换其X值
3.交换在2维数组中的对应位置
4.两个卡片均执行move方法
protected void tran(Card card,int dir) {
Card tempCard=curCard;
curCard.setSelected(false);
curCard= null;
int x = card.getX();
int y = card.getY();
int x1 = tempCard.getX();
int y1 = tempCard.getY();
if(dir==1){//横向交换,对应横向移动
card.setY(y1);
tempCard.setY(y);
}else {//纵向交换,对应纵向移动
card.setX(x1);
tempCard.setX(x);
}
//交换在2维数组中的对应位置
cards[x][y]= tempCard;
cards[x1][y1]= card;
card.move(dir);
tempCard.move(dir);
}
实现Card类中的move方法
1.记录最新后的位置odx、ody;
2.更odx、ody计算图片运动方向。
public void move(int d) {
this.moveFlag=d;
int dis= 0;
if(this.moveFlag==1){//横向交换,对应横向移动
this.odx=40+y*(32+3)+10;
dis = this.odx-this.dx;
}else {
this.ody=35+x*(32+3)+10;
dis = this.ody-this.dy;
}
if(dis>0){//向下运动 、向右运动
dir = 1;
}else {
dir = -1;
}
}
修改draw方法
1.根据移动方向和速度更新dx、dy,用来达到移动的目的。
2.当dx>=odx或dx<=odx、dy>=ody或dy<=ody 达到最大移动位置。
3.达到最大移动位置后,要进行消除逻辑处理,写入空方法clear
4.加入边框,表示卡片被选择状态。
//绘制
public void draw(Graphics g) {
int index = this.index;
if(moveFlag!=0){
if(this.moveFlag==1){//横向移动
dx += dir*speed;//dx修改
if(dir>0){
if(dx>=odx){//运动到既定位置,停止
dx = odx;
moveFlag=0;
clear();
}
}else{//运动到既定位置,停止
if(dx<=odx){
dx = odx;
moveFlag=0;
clear();
}
}
}else {//纵向移动
dy += dir*speed;
if(dir>0){
if(dy>=ody){//运动到既定位置,停止
dy = ody;
moveFlag=0;
clear();
}
}else{
if(dy<=ody){//运动到既定位置,停止
dy = ody;
moveFlag=0;
clear();
}
}
}
}
//index 默认是0,就是从图片中截取第一个
int sx1 = index*32;
int sy1 = 0;
//截取的右下角计算
int sx2 = (index+1)*32;
int sy2 = 32;
g.drawImage(this.image,dx, dy,dx+width,dy+height,sx1,sy1,sx2,sy2 ,null );
if(selected){
//绘制边框
Color oColor = g.getColor();
g.setColor(Color.pink);
g.drawRect(dx, dy, 32, 32);
g.setColor(oColor);
}
}
X方向消除
以当前为中心,向左向右逐个判断,遇到不同的则返回,遇到相同的则计数器加1,当左边+自己+右边 大于三,则横向需要消除。
//x方向左边计算
private int computedLeftX() {
int res = 0;
Card card;
//从当前卡片的前一个位置开始往前计算,遇到与当前卡片不是同一类型就直接返回
for (int i = this.y-1; i >=0; i--) {
card = panel.cards[this.x][i];
if(card==null) continue;
if(card.pIndex==this.pIndex){
res++;
}else {
break;
}
}
return res;
}
//x方向右边计算
private int computedRightX() {
int res = 0;
Card card;
//从当前卡片的后一个位置开始往后计算,遇到与当前卡片不是同一类型就直接返回
for (int i = this.y+1; i < panel.COLS; i++) {
card = panel.cards[this.x][i];
if(card==null) continue;
if(card.pIndex==this.pIndex){
res++;
}else {
break;
}
}
return res;
}
让消除行的上方每一个,依次往下落一格
上方如果没有了,则新生成一个卡片。
//当前卡片上方全部下落一格
private void down(Card c){
Card lastCard;
//x的循环,表示往上上一个个的取
for (int i = c.x; i >=0; i--) {
if(i==0){//新创建
createCard(0,c.y);
continue;
}else{
lastCard = panel.cards[i-1][c.y];
}
if(lastCard==null) {//新创建
createCard(i-1,c.y);
}
//向下移动
lastCard.setX(i);
panel.cards[i][c.y]= lastCard;
lastCard.move(2);
}
}
于是X方向消除代码
//x方向消除
private int clearX(int left,int right,int type) {
MusicPlayer.disappearMisic();
//左边消除
Card card ;
for (int i = this.y-1; i >= this.y-left ; i--) {
card = panel.cards[this.x][i];
if(card==null) continue;
card.alive=false;
panel.cards[this.x][i]=null;
down(card);
}
for (int i = this.y+1; i <= this.y+right ; i++) {
card = panel.cards[this.x][i];
if(card==null) continue;
card.alive=false;
panel.cards[this.x][i]=null;
down(card);
}
if(type==1){//自己也消除
this.alive=false;
panel.cards[this.x][this.y]=null;
down(this);
}
return 0;
}
加入Y方向消除代码
private void clearY(int uCount, int dCount) {
MusicPlayer.disappearMisic();
int y = this.y;
int count = uCount+1+dCount;
int maxX = this.x+dCount;
int minX = this.x-uCount;
Card card;
while(count>0){
card = panel.cards[maxX][y];
card.alive=false;
panel.cards[maxX][y]=null;
down(card);
count--;
}
}
加入积分、游戏结束、重新开始、音效等就完成了
看到这里的大佬,动动发财的小手 点赞 + 回复 + 收藏,能【 关注 】一波就更好了。
想要代码的 加微信 或 私聊 我!
为了帮助更多小白从零进阶 Java 工程师,从CSDN官方那边搞来了一套 《Java 工程师学习成长知识图谱》,尺寸 870mm x 560mm,展开后有一张办公桌大小,也可以折叠成一本书的尺寸,原件129元现价 29 元,先到先得,有兴趣的小伙伴可以了解一下!
★ 更多精彩
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK